forked from Shinonome/dots-hyprland
notifications: groups
This commit is contained in:
@@ -79,7 +79,7 @@ function mix(color1, color2, percentage) {
|
|||||||
* @param {number} percentage - The amount to transparentize (0-1).
|
* @param {number} percentage - The amount to transparentize (0-1).
|
||||||
* @returns {Qt.rgba} The resulting color.
|
* @returns {Qt.rgba} The resulting color.
|
||||||
*/
|
*/
|
||||||
function transparentize(color, percentage) {
|
function transparentize(color, percentage = 1) {
|
||||||
var c = Qt.color(color);
|
var c = Qt.color(color);
|
||||||
return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage));
|
return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ MouseArea { // Flick to dismiss
|
|||||||
|
|
||||||
onPressed: (mouse) => {
|
onPressed: (mouse) => {
|
||||||
if (!root.interactive) {
|
if (!root.interactive) {
|
||||||
mouse.accepted = false;
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
mouse.accepted = false;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mouse.button === Qt.LeftButton) {
|
if (mouse.button === Qt.LeftButton) {
|
||||||
@@ -40,7 +42,6 @@ MouseArea { // Flick to dismiss
|
|||||||
}
|
}
|
||||||
onReleased: (mouse) => {
|
onReleased: (mouse) => {
|
||||||
if (!root.interactive) {
|
if (!root.interactive) {
|
||||||
mouse.accepted = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dragging = false
|
dragging = false
|
||||||
@@ -51,7 +52,6 @@ MouseArea { // Flick to dismiss
|
|||||||
}
|
}
|
||||||
onPositionChanged: (mouse) => {
|
onPositionChanged: (mouse) => {
|
||||||
if (!root.interactive) {
|
if (!root.interactive) {
|
||||||
mouse.accepted = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mouse.buttons & Qt.LeftButton) {
|
if (mouse.buttons & Qt.LeftButton) {
|
||||||
@@ -64,7 +64,6 @@ MouseArea { // Flick to dismiss
|
|||||||
}
|
}
|
||||||
onCanceled: (mouse) => {
|
onCanceled: (mouse) => {
|
||||||
if (!root.interactive) {
|
if (!root.interactive) {
|
||||||
mouse.accepted = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
released(mouse);
|
released(mouse);
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import "root:/modules/common"
|
||||||
|
import "./notification_utils.js" as NotificationUtils
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
|
||||||
|
Rectangle { // App icon
|
||||||
|
id: root
|
||||||
|
property var appIcon: ""
|
||||||
|
property var summary: ""
|
||||||
|
property var urgency: NotificationUrgency.Normal
|
||||||
|
property var image: ""
|
||||||
|
property real size: 45
|
||||||
|
property real materialIconScale: 0.57
|
||||||
|
property real appIconScale: 0.7
|
||||||
|
property real smallAppIconScale: 0.49
|
||||||
|
property real materialIconSize: size * materialIconScale
|
||||||
|
property real appIconSize: size * appIconScale
|
||||||
|
property real smallAppIconSize: size * smallAppIconScale
|
||||||
|
|
||||||
|
implicitWidth: size
|
||||||
|
implicitHeight: size
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
color: Appearance.m3colors.m3secondaryContainer
|
||||||
|
Loader {
|
||||||
|
id: materialSymbolLoader
|
||||||
|
active: root.appIcon == ""
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceComponent: MaterialSymbol {
|
||||||
|
text: {
|
||||||
|
const defaultIcon = NotificationUtils.findSuitableMaterialSymbol("")
|
||||||
|
const guessedIcon = NotificationUtils.findSuitableMaterialSymbol(root.summary)
|
||||||
|
return (root.urgency == NotificationUrgency.Critical && guessedIcon === defaultIcon) ?
|
||||||
|
"release_alert" : guessedIcon
|
||||||
|
}
|
||||||
|
anchors.fill: parent
|
||||||
|
color: (root.urgency == NotificationUrgency.Critical) ?
|
||||||
|
ColorUtils.mix(Appearance.m3colors.m3onSecondary, Appearance.m3colors.m3onSecondaryContainer, 0.1) :
|
||||||
|
Appearance.m3colors.m3onSecondaryContainer
|
||||||
|
iconSize: root.materialIconSize
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loader {
|
||||||
|
id: appIconLoader
|
||||||
|
active: root.image == "" && root.appIcon != ""
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceComponent: IconImage {
|
||||||
|
id: appIconImage
|
||||||
|
implicitSize: root.appIconSize
|
||||||
|
asynchronous: true
|
||||||
|
source: Quickshell.iconPath(root.appIcon, "image-missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loader {
|
||||||
|
id: notifImageLoader
|
||||||
|
active: root.image != ""
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceComponent: Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
Image {
|
||||||
|
id: notifImage
|
||||||
|
anchors.fill: parent
|
||||||
|
readonly property int size: parent.width
|
||||||
|
|
||||||
|
source: root.image
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
cache: false
|
||||||
|
antialiasing: true
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
width: size
|
||||||
|
height: size
|
||||||
|
sourceSize.width: size
|
||||||
|
sourceSize.height: size
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: OpacityMask {
|
||||||
|
maskSource: Rectangle {
|
||||||
|
width: notifImage.size
|
||||||
|
height: notifImage.size
|
||||||
|
radius: Appearance.rounding.full
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loader {
|
||||||
|
id: notifImageAppIconLoader
|
||||||
|
active: root.appIcon != ""
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.right: parent.right
|
||||||
|
sourceComponent: IconImage {
|
||||||
|
implicitSize: root.smallAppIconSize
|
||||||
|
asynchronous: true
|
||||||
|
source: Quickshell.iconPath(root.appIcon, "image-missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import "root:/modules/common"
|
||||||
|
import "root:/services"
|
||||||
|
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||||
|
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||||
|
import "./notification_utils.js" as NotificationUtils
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A group of notifications from the same app.
|
||||||
|
* Similar to Android's notifications
|
||||||
|
*/
|
||||||
|
Item { // Notification group area
|
||||||
|
id: root
|
||||||
|
property var notificationGroup
|
||||||
|
property var notifications: notificationGroup.notifications
|
||||||
|
property int notificationCount: notifications.length
|
||||||
|
property bool multipleNotifications: notificationCount > 1
|
||||||
|
property bool expanded: false
|
||||||
|
property bool popup: false
|
||||||
|
property real padding: 10
|
||||||
|
implicitHeight: background.implicitHeight
|
||||||
|
|
||||||
|
property real dragConfirmThreshold: 70 // Drag further to discard notification
|
||||||
|
property real dismissOvershoot: 20 // Account for gaps and bouncy animations
|
||||||
|
property var qmlParent: root.parent.parent // There's something between this and the parent ListView
|
||||||
|
property var parentDragIndex: qmlParent.dragIndex
|
||||||
|
property var parentDragDistance: qmlParent.dragDistance
|
||||||
|
property var dragIndexDiff: Math.abs(parentDragIndex - index)
|
||||||
|
property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) :
|
||||||
|
parentDragDistance > dragConfirmThreshold ? 0 :
|
||||||
|
dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) :
|
||||||
|
dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0
|
||||||
|
|
||||||
|
function destroyWithAnimation() {
|
||||||
|
root.qmlParent.resetDrag()
|
||||||
|
background.anchors.leftMargin = background.anchors.leftMargin; // Break binding
|
||||||
|
destroyAnimation.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation { // Drag finish animation
|
||||||
|
id: destroyAnimation
|
||||||
|
running: false
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: background.anchors
|
||||||
|
property: "leftMargin"
|
||||||
|
to: root.width + root.dismissOvershoot
|
||||||
|
duration: Appearance.animation.elementMove.duration
|
||||||
|
easing.type: Appearance.animation.elementMove.type
|
||||||
|
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||||
|
}
|
||||||
|
onFinished: () => {
|
||||||
|
root.notifications.forEach((notif) => {
|
||||||
|
Notifications.discardNotification(notif.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpanded() {
|
||||||
|
if (expanded) implicitHeightAnim.enabled = true;
|
||||||
|
else implicitHeightAnim.enabled = false;
|
||||||
|
root.expanded = !root.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
DragManager { // Drag manager
|
||||||
|
id: dragManager
|
||||||
|
anchors.fill: parent
|
||||||
|
interactive: !expanded
|
||||||
|
automaticallyReset: false
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
root.toggleExpanded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDraggingChanged: () => {
|
||||||
|
if (dragging) {
|
||||||
|
root.qmlParent.dragIndex = root.index ?? root.parent.children.indexOf(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragDiffXChanged: () => {
|
||||||
|
root.qmlParent.dragDistance = dragDiffX;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragReleased: (diffX, diffY) => {
|
||||||
|
if (diffX > root.dragConfirmThreshold)
|
||||||
|
root.destroyWithAnimation();
|
||||||
|
else
|
||||||
|
dragManager.resetDrag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Rectangle { // Background of the notification
|
||||||
|
id: background
|
||||||
|
anchors.left: parent.left
|
||||||
|
width: parent.width
|
||||||
|
color: Appearance.m3colors.m3surfaceContainer
|
||||||
|
radius: Appearance.rounding.normal
|
||||||
|
anchors.leftMargin: root.xOffset
|
||||||
|
|
||||||
|
Behavior on anchors.leftMargin {
|
||||||
|
enabled: !dragManager.dragging
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Appearance.animation.elementMove.duration
|
||||||
|
easing.type: Appearance.animation.elementMove.type
|
||||||
|
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
implicitHeight: expanded ?
|
||||||
|
row.implicitHeight + padding * 2 :
|
||||||
|
Math.min(80, row.implicitHeight + padding * 2)
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
id: implicitHeightAnim
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
source: background
|
||||||
|
anchors.fill: background
|
||||||
|
shadowEnabled: popup
|
||||||
|
shadowColor: Appearance.colors.colShadow
|
||||||
|
shadowVerticalOffset: 1
|
||||||
|
shadowBlur: 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout { // Left column for icon, right column for content
|
||||||
|
id: row
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: root.padding
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
NotificationAppIcon { // Icons
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
Layout.fillWidth: false
|
||||||
|
|
||||||
|
appIcon: notificationGroup.appIcon
|
||||||
|
summary: notificationGroup.notifications[root.notificationCount - 1].summary
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout { // Content
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: expanded ? 5 : 0
|
||||||
|
Behavior on spacing {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout { // App name (or summary when there's only 1 notif) and time
|
||||||
|
id: topRow
|
||||||
|
spacing: 0
|
||||||
|
property real fontSize: Appearance.font.pixelSize.smaller
|
||||||
|
property bool showAppName: root.multipleNotifications
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: appName
|
||||||
|
text: topRow.showAppName ?
|
||||||
|
notificationGroup.appName :
|
||||||
|
notificationGroup.notifications[0].summary
|
||||||
|
font.pixelSize: topRow.showAppName ?
|
||||||
|
topRow.fontSize :
|
||||||
|
Appearance.font.pixelSize.small
|
||||||
|
color: topRow.showAppName ?
|
||||||
|
Appearance.colors.colSubtext :
|
||||||
|
Appearance.colors.colOnLayer2
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
id: timeText
|
||||||
|
text: " • " + NotificationUtils.getFriendlyNotifTimeString(notificationGroup.time)
|
||||||
|
font.pixelSize: topRow.fontSize
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
Layout.alignment: Qt.AlignRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
NotificationGroupExpandButton {
|
||||||
|
count: root.notificationCount
|
||||||
|
expanded: root.expanded
|
||||||
|
fontSize: topRow.fontSize
|
||||||
|
onClicked: { root.toggleExpanded() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledListView { // Notification body (expanded)
|
||||||
|
id: notificationsColumn
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: expanded ? 5 : 3
|
||||||
|
// clip: true
|
||||||
|
interactive: false
|
||||||
|
Behavior on spacing {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
model: ScriptModel {
|
||||||
|
// values: root.expanded ? root.notifications : root.notifications.slice(0, 2)
|
||||||
|
values: root.notifications.slice().reverse()
|
||||||
|
}
|
||||||
|
delegate: NotificationItem {
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
notificationObject: modelData
|
||||||
|
expanded: root.expanded
|
||||||
|
onlyNotification: (root.notificationCount === 1)
|
||||||
|
opacity: (!root.expanded && index == 1 && root.notificationCount > 2) ? 0.5 : 1
|
||||||
|
visible: root.expanded || (index < 2)
|
||||||
|
anchors.left: parent?.left
|
||||||
|
anchors.right: parent?.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import "root:/modules/common"
|
||||||
|
import "root:/services"
|
||||||
|
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
|
||||||
|
RippleButton { // Expand button
|
||||||
|
id: root
|
||||||
|
required property int count
|
||||||
|
required property bool expanded
|
||||||
|
property real fontSize: Appearance.font.pixelSize.small
|
||||||
|
implicitHeight: fontSize + 4 * 2
|
||||||
|
implicitWidth: Math.max(contentItem.implicitWidth + 5 * 2, 30)
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
Layout.fillHeight: false
|
||||||
|
|
||||||
|
buttonRadius: Appearance.rounding.full
|
||||||
|
colBackground: ColorUtils.mix(Appearance.colors.colLayer2, Appearance.colors.colLayer2Hover, 0.5)
|
||||||
|
colBackgroundHover: Appearance.colors.colLayer2Hover
|
||||||
|
colRipple: Appearance.colors.colLayer2Active
|
||||||
|
|
||||||
|
contentItem: Item {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitWidth: contentRow.implicitWidth
|
||||||
|
RowLayout {
|
||||||
|
id: contentRow
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 3
|
||||||
|
StyledText {
|
||||||
|
Layout.leftMargin: 4
|
||||||
|
visible: root.count > 1
|
||||||
|
text: root.count
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
}
|
||||||
|
MaterialSymbol {
|
||||||
|
text: "keyboard_arrow_down"
|
||||||
|
iconSize: Appearance.font.pixelSize.normal
|
||||||
|
color: Appearance.colors.colOnLayer2
|
||||||
|
rotation: expanded ? 180 : 0
|
||||||
|
Behavior on rotation {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import "root:/modules/common"
|
||||||
|
import "root:/services"
|
||||||
|
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||||
|
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||||
|
import "./notification_utils.js" as NotificationUtils
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
|
||||||
|
Item { // Notification item area
|
||||||
|
id: root
|
||||||
|
property var notificationObject
|
||||||
|
property bool expanded: false
|
||||||
|
property bool onlyNotification: false
|
||||||
|
property real fontSize: Appearance.font.pixelSize.small
|
||||||
|
property real padding: 8
|
||||||
|
|
||||||
|
property real dragConfirmThreshold: 70 // Drag further to discard notification
|
||||||
|
property real dismissOvershoot: 20 // Account for gaps and bouncy animations
|
||||||
|
property var qmlParent: root?.parent?.parent // There's something between this and the parent ListView
|
||||||
|
property var parentDragIndex: qmlParent?.dragIndex ?? -1
|
||||||
|
property var parentDragDistance: qmlParent?.dragDistance ?? 0
|
||||||
|
property var dragIndexDiff: Math.abs(parentDragIndex - index)
|
||||||
|
property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) :
|
||||||
|
parentDragDistance > dragConfirmThreshold ? 0 :
|
||||||
|
dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) :
|
||||||
|
dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0
|
||||||
|
|
||||||
|
implicitHeight: background.implicitHeight
|
||||||
|
|
||||||
|
function destroyWithAnimation() {
|
||||||
|
root.qmlParent.resetDrag()
|
||||||
|
background.anchors.leftMargin = background.anchors.leftMargin; // Break binding
|
||||||
|
destroyAnimation.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation { // Drag finish animation
|
||||||
|
id: destroyAnimation
|
||||||
|
running: false
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
target: background.anchors
|
||||||
|
property: "leftMargin"
|
||||||
|
to: root.width + root.dismissOvershoot
|
||||||
|
duration: Appearance.animation.elementMove.duration
|
||||||
|
easing.type: Appearance.animation.elementMove.type
|
||||||
|
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||||
|
}
|
||||||
|
onFinished: () => {
|
||||||
|
Notifications.discardNotification(notificationObject.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DragManager { // Drag manager
|
||||||
|
id: dragManager
|
||||||
|
anchors.fill: parent
|
||||||
|
interactive: expanded
|
||||||
|
automaticallyReset: false
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
|
||||||
|
onPressAndHold: (mouse) => {
|
||||||
|
if (mouse.button === Qt.LeftButton) {
|
||||||
|
Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(notificationObject.body)}'`)
|
||||||
|
notificationSummaryText.text = String.format(qsTr("{0} (copied)"), notificationObject.summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onDraggingChanged: () => {
|
||||||
|
if (dragging) {
|
||||||
|
root.qmlParent.dragIndex = root.index ?? root.parent.children.indexOf(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragDiffXChanged: () => {
|
||||||
|
root.qmlParent.dragDistance = dragDiffX;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragReleased: (diffX, diffY) => {
|
||||||
|
if (diffX > root.dragConfirmThreshold)
|
||||||
|
root.destroyWithAnimation();
|
||||||
|
else
|
||||||
|
dragManager.resetDrag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle { // Background of notification item
|
||||||
|
id: background
|
||||||
|
width: parent.width
|
||||||
|
anchors.left: parent.left
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
anchors.leftMargin: root.xOffset
|
||||||
|
|
||||||
|
Behavior on anchors.leftMargin {
|
||||||
|
enabled: !dragManager.dragging
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Appearance.animation.elementMove.duration
|
||||||
|
easing.type: Appearance.animation.elementMove.type
|
||||||
|
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color: expanded ?
|
||||||
|
(notificationObject.urgency == NotificationUrgency.Critical) ?
|
||||||
|
ColorUtils.mix(Appearance.m3colors.m3secondaryContainer, Appearance.colors.colLayer2, 0.35) :
|
||||||
|
(Appearance.m3colors.m3surfaceContainerHigh) :
|
||||||
|
ColorUtils.transparentize(Appearance.m3colors.m3surfaceContainerHighest)
|
||||||
|
|
||||||
|
implicitHeight: expanded ? (contentColumn.implicitHeight + padding * 2) : summaryRow.implicitHeight
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout { // Content column
|
||||||
|
id: contentColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: expanded ? root.padding : 0
|
||||||
|
spacing: 3
|
||||||
|
|
||||||
|
Behavior on anchors.margins {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout { // Summary row
|
||||||
|
id: summaryRow
|
||||||
|
visible: !root.onlyNotification || !root.expanded
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: summaryText.implicitHeight
|
||||||
|
// Layout.fillWidth: true
|
||||||
|
StyledText {
|
||||||
|
id: summaryText
|
||||||
|
visible: !root.onlyNotification
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
color: Appearance.colors.colOnLayer2
|
||||||
|
elide: Text.ElideRight
|
||||||
|
text: root.notificationObject.summary || ""
|
||||||
|
}
|
||||||
|
StyledText {
|
||||||
|
opacity: !root.expanded ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
Behavior on opacity {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
elide: Text.ElideRight
|
||||||
|
textFormat: Text.StyledText
|
||||||
|
text: notificationObject.body.replace(/<img/g, "\n <img").split("\n")[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout { // Expanded content
|
||||||
|
Layout.fillWidth: true
|
||||||
|
opacity: root.expanded ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
|
StyledText { // Notification body (expanded)
|
||||||
|
id: notificationBodyText
|
||||||
|
Behavior on opacity {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font.pixelSize: root.fontSize
|
||||||
|
color: Appearance.colors.colSubtext
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
elide: Text.ElideRight
|
||||||
|
textFormat: Text.RichText
|
||||||
|
text: `<style>img{max-width:${notificationBodyText.width}px;}</style>` +
|
||||||
|
`${notificationObject.body.replace(/\n/g, "<br/>")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable { // Notification actions
|
||||||
|
id: actionsFlickable
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: actionRowLayout.implicitHeight
|
||||||
|
contentWidth: actionRowLayout.implicitWidth
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
Behavior on height {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: actionRowLayout
|
||||||
|
Layout.alignment: Qt.AlignBottom
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: actionRepeater
|
||||||
|
model: notificationObject.actions
|
||||||
|
NotificationActionButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
buttonText: modelData.text
|
||||||
|
urgency: notificationObject.urgency
|
||||||
|
onClicked: {
|
||||||
|
Notifications.attemptInvokeAction(notificationObject.id, modelData.identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationActionButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
urgency: notificationObject.urgency
|
||||||
|
implicitWidth: (notificationObject.actions.length == 0) ? ((actionsFlickable.width - actionRowLayout.spacing) / 2) :
|
||||||
|
(contentItem.implicitWidth + leftPadding + rightPadding)
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(notificationObject.body)}'`)
|
||||||
|
copyIcon.text = "inventory"
|
||||||
|
copyIconTimer.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: copyIconTimer
|
||||||
|
interval: 1500
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
copyIcon.text = "content_copy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: MaterialSymbol {
|
||||||
|
id: copyIcon
|
||||||
|
iconSize: Appearance.font.pixelSize.large
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
|
||||||
|
Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
|
||||||
|
text: "content_copy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationActionButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
buttonText: qsTr("Close")
|
||||||
|
urgency: notificationObject.urgency
|
||||||
|
implicitWidth: (notificationObject.actions.length == 0) ? ((actionsFlickable.width - actionRowLayout.spacing) / 2) :
|
||||||
|
(contentItem.implicitWidth + leftPadding + rightPadding)
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.destroyWithAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: MaterialSymbol {
|
||||||
|
iconSize: Appearance.font.pixelSize.large
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
|
||||||
|
Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
|
||||||
|
text: "close"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import "root:/"
|
||||||
|
import "root:/modules/common/"
|
||||||
|
import "root:/modules/common/widgets"
|
||||||
|
import "root:/services"
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
|
||||||
|
StyledListView { // Scrollable window
|
||||||
|
id: root
|
||||||
|
property bool popup: false
|
||||||
|
|
||||||
|
spacing: 3
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.popup ? Notifications.popupAppNameList : Notifications.appNameList
|
||||||
|
}
|
||||||
|
delegate: NotificationGroup {
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
popup: root.popup
|
||||||
|
anchors.left: parent?.left
|
||||||
|
anchors.right: parent?.right
|
||||||
|
notificationGroup: popup ?
|
||||||
|
Notifications.popupGroupsByAppName[modelData] :
|
||||||
|
Notifications.groupsByAppName[modelData]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,577 +0,0 @@
|
|||||||
import "root:/modules/common"
|
|
||||||
import "root:/services"
|
|
||||||
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
|
||||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
|
||||||
import Qt5Compat.GraphicalEffects
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import QtQuick.Effects
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.Services.Notifications
|
|
||||||
import "./notification_utils.js" as NotificationUtils
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
property var notificationObject
|
|
||||||
property bool popup: false
|
|
||||||
property bool expanded: false
|
|
||||||
property bool enableAnimation: true
|
|
||||||
property int notificationListSpacing: 5
|
|
||||||
property int defaultTimeoutValue: 5000
|
|
||||||
|
|
||||||
property var notificationXAnimation: Appearance.animation.elementMoveEnter
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
clip: !popup
|
|
||||||
|
|
||||||
implicitHeight: notificationColumnLayout.implicitHeight + notificationListSpacing
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
if (popup) timeoutTimer.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: timeoutTimer
|
|
||||||
interval: notificationObject.expireTimeout ?? root.defaultTimeoutValue
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
root.notificationXAnimation = Appearance.animation.elementMoveExit
|
|
||||||
Notifications.timeoutNotification(notificationObject.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroyWithAnimation(delay = 0) {
|
|
||||||
destroyTimer0.interval = delay
|
|
||||||
destroyTimer0.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleExpanded() {
|
|
||||||
root.enableAnimation = true
|
|
||||||
notificationRowWrapper.anchors.bottom = undefined
|
|
||||||
root.expanded = !root.expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: destroyTimer0
|
|
||||||
interval: 0
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
notificationRowWrapper.anchors.left = undefined
|
|
||||||
notificationRowWrapper.anchors.right = undefined
|
|
||||||
notificationRowWrapper.anchors.fill = undefined
|
|
||||||
notificationBackground.anchors.left = undefined
|
|
||||||
notificationBackground.anchors.right = undefined
|
|
||||||
notificationBackground.anchors.fill = undefined
|
|
||||||
notificationRowWrapper.x = width + (Appearance.sizes.hyprlandGapsOut + Appearance.sizes.elevationMargin) * 2 // Account for shadow
|
|
||||||
notificationBackground.x = width + (Appearance.sizes.hyprlandGapsOut + Appearance.sizes.elevationMargin) * 2 // Account for shadow
|
|
||||||
destroyTimer1.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: destroyTimer1
|
|
||||||
interval: notificationXAnimation.duration
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
notificationRowWrapper.anchors.top = undefined
|
|
||||||
notificationRowWrapper.anchors.bottom = root.bottom
|
|
||||||
Notifications.discardNotification(notificationObject.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
// Middle click to close
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
|
||||||
onClicked: (mouse) => {
|
|
||||||
if (mouse.button == Qt.MiddleButton)
|
|
||||||
root.destroyWithAnimation()
|
|
||||||
else if (mouse.button == Qt.RightButton)
|
|
||||||
root.toggleExpanded()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flick right to dismiss/discard
|
|
||||||
property real startX: 0
|
|
||||||
property real dragStartThreshold: 10
|
|
||||||
property real dragConfirmThreshold: 70
|
|
||||||
property bool dragStarted: false
|
|
||||||
|
|
||||||
onPressed: (mouse) => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
startX = mouse.x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onPressAndHold: (mouse) => {
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(notificationObject.body)}'`)
|
|
||||||
notificationSummaryText.text = String.format(qsTr("{0} (copied)"), notificationObject.summary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onDragStartedChanged: () => {
|
|
||||||
// Prevent drag focus being shifted to parent flickable
|
|
||||||
if (root.parent.parent.parent.interactive !== undefined) root.parent.parent.parent.interactive = !dragStarted
|
|
||||||
root.enableAnimation = !dragStarted
|
|
||||||
}
|
|
||||||
onReleased: (mouse) => {
|
|
||||||
dragStarted = false
|
|
||||||
if (mouse.button === Qt.LeftButton) {
|
|
||||||
if (notificationRowWrapper.x > dragConfirmThreshold) {
|
|
||||||
root.notificationXAnimation = Appearance.animation.elementMoveEnter
|
|
||||||
root.destroyWithAnimation()
|
|
||||||
} else {
|
|
||||||
// Animate back if not far enough
|
|
||||||
root.notificationXAnimation = Appearance.animation.elementMoveFast
|
|
||||||
notificationRowWrapper.x = 0
|
|
||||||
notificationBackground.x = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onCanceled: (mouse) => {
|
|
||||||
dragStarted = false
|
|
||||||
if (notificationRowWrapper.x > dragConfirmThreshold) {
|
|
||||||
root.notificationXAnimation = Appearance.animation.elementMoveEnter
|
|
||||||
root.destroyWithAnimation()
|
|
||||||
} else {
|
|
||||||
// Animate back if not far enough
|
|
||||||
root.notificationXAnimation = Appearance.animation.elementMoveFast
|
|
||||||
notificationRowWrapper.x = 0
|
|
||||||
notificationBackground.x = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPositionChanged: (mouse) => {
|
|
||||||
if (mouse.buttons & Qt.LeftButton) {
|
|
||||||
let dx = mouse.x - startX
|
|
||||||
if (dragStarted || dx > dragStartThreshold) {
|
|
||||||
dragStarted = true
|
|
||||||
notificationRowWrapper.anchors.left = undefined
|
|
||||||
notificationRowWrapper.anchors.right = undefined
|
|
||||||
notificationRowWrapper.anchors.fill = undefined
|
|
||||||
notificationBackground.anchors.left = undefined
|
|
||||||
notificationBackground.anchors.right = undefined
|
|
||||||
notificationBackground.anchors.fill = undefined
|
|
||||||
notificationRowWrapper.x = Math.max(0, dx)
|
|
||||||
notificationBackground.x = Math.max(0, dx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background
|
|
||||||
Item {
|
|
||||||
id: notificationBackgroundWrapper
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.topMargin: notificationListSpacing
|
|
||||||
implicitHeight: notificationColumnLayout.implicitHeight + notificationListSpacing
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: notificationBackground
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
// anchors.top: parent.top
|
|
||||||
implicitHeight: notificationColumnLayout.implicitHeight
|
|
||||||
|
|
||||||
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
|
|
||||||
ColorUtils.mix(Appearance.m3colors.m3secondaryContainer, Appearance.colors.colLayer2, 0.35) : Appearance.colors.colLayer2
|
|
||||||
radius: Appearance.rounding.normal
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: MultiEffect {
|
|
||||||
source: notificationBackground
|
|
||||||
anchors.fill: notificationBackground
|
|
||||||
shadowEnabled: popup
|
|
||||||
shadowColor: Appearance.colors.colShadow
|
|
||||||
shadowVerticalOffset: 1
|
|
||||||
shadowBlur: 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
enabled: enableAnimation
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.notificationXAnimation.duration
|
|
||||||
easing.type: root.notificationXAnimation.type
|
|
||||||
easing.bezierCurve: root.notificationXAnimation.bezierCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: notificationRowWrapper
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
// anchors.top: parent.top
|
|
||||||
implicitHeight: notificationColumnLayout.implicitHeight + notificationListSpacing
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
enabled: enableAnimation
|
|
||||||
NumberAnimation {
|
|
||||||
duration: root.notificationXAnimation.duration
|
|
||||||
easing.type: root.notificationXAnimation.type
|
|
||||||
easing.bezierCurve: root.notificationXAnimation.bezierCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: notificationColumnLayout
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
spacing: 0
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
implicitHeight: notificationRowLayout.implicitHeight
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
enabled: enableAnimation
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Appearance.animation.elementMoveFast.duration
|
|
||||||
easing.type: Appearance.animation.elementMoveFast.type
|
|
||||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: notificationRowLayout
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
Rectangle { // App icon
|
|
||||||
id: iconRectangle
|
|
||||||
implicitWidth: 47
|
|
||||||
implicitHeight: 47
|
|
||||||
Layout.leftMargin: 10
|
|
||||||
Layout.topMargin: 10
|
|
||||||
Layout.bottomMargin: 10
|
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
Layout.fillWidth: false
|
|
||||||
radius: Appearance.rounding.full
|
|
||||||
color: Appearance.m3colors.m3secondaryContainer
|
|
||||||
Loader {
|
|
||||||
id: materialSymbolLoader
|
|
||||||
active: notificationObject.appIcon == ""
|
|
||||||
anchors.fill: parent
|
|
||||||
sourceComponent: MaterialSymbol {
|
|
||||||
text: {
|
|
||||||
const defaultIcon = NotificationUtils.findSuitableMaterialSymbol("")
|
|
||||||
const guessedIcon = NotificationUtils.findSuitableMaterialSymbol(notificationObject.summary)
|
|
||||||
return (notificationObject.urgency == NotificationUrgency.Critical && guessedIcon === defaultIcon) ?
|
|
||||||
"release_alert" : guessedIcon
|
|
||||||
}
|
|
||||||
anchors.fill: parent
|
|
||||||
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
|
|
||||||
ColorUtils.mix(Appearance.m3colors.m3onSecondary, Appearance.m3colors.m3onSecondaryContainer, 0.1) :
|
|
||||||
Appearance.m3colors.m3onSecondaryContainer
|
|
||||||
iconSize: 27
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loader {
|
|
||||||
id: appIconLoader
|
|
||||||
active: notificationObject.image == "" && notificationObject.appIcon != ""
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceComponent: IconImage {
|
|
||||||
implicitSize: 33
|
|
||||||
asynchronous: true
|
|
||||||
source: Quickshell.iconPath(notificationObject.appIcon, "image-missing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loader {
|
|
||||||
id: notifImageLoader
|
|
||||||
active: notificationObject.image != ""
|
|
||||||
anchors.fill: parent
|
|
||||||
sourceComponent: Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
Image {
|
|
||||||
id: notifImage
|
|
||||||
anchors.fill: parent
|
|
||||||
readonly property int size: parent.width
|
|
||||||
|
|
||||||
source: notificationObject?.image
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
cache: false
|
|
||||||
antialiasing: true
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
width: size
|
|
||||||
height: size
|
|
||||||
sourceSize.width: size
|
|
||||||
sourceSize.height: size
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: OpacityMask {
|
|
||||||
maskSource: Rectangle {
|
|
||||||
width: notifImage.size
|
|
||||||
height: notifImage.size
|
|
||||||
radius: Appearance.rounding.full
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loader {
|
|
||||||
id: notifImageAppIconLoader
|
|
||||||
active: notificationObject.appIcon != ""
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.right: parent.right
|
|
||||||
sourceComponent: IconImage {
|
|
||||||
implicitSize: 23
|
|
||||||
asynchronous: true
|
|
||||||
source: Quickshell.iconPath(notificationObject.appIcon, "image-missing")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout { // Notification content
|
|
||||||
spacing: 0
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
RowLayout { // Row of summary, time and expand button
|
|
||||||
Layout.topMargin: 10
|
|
||||||
Layout.leftMargin: 10
|
|
||||||
Layout.rightMargin: 10
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
StyledText { // Summary
|
|
||||||
id: notificationSummaryText
|
|
||||||
Layout.fillWidth: true
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
verticalAlignment: Text.AlignBottom
|
|
||||||
font.pixelSize: Appearance.font.pixelSize.normal
|
|
||||||
color: Appearance.colors.colOnLayer2
|
|
||||||
text: notificationObject.summary
|
|
||||||
wrapMode: expanded ? Text.Wrap : Text.NoWrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
CircularProgress {
|
|
||||||
id: notificationProgress
|
|
||||||
visible: popup
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
lineWidth: 2
|
|
||||||
value: popup ? 1 : 0
|
|
||||||
size: 20
|
|
||||||
animationDuration: notificationObject.expireTimeout ?? root.defaultTimeoutValue
|
|
||||||
easingType: Easing.Linear
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
value = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText { // Time
|
|
||||||
id: notificationTimeText
|
|
||||||
Layout.fillWidth: false
|
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
Layout.topMargin: 3
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
|
||||||
color: Appearance.m3colors.m3outline
|
|
||||||
text: NotificationUtils.getFriendlyNotifTimeString(notificationObject.time)
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: DateTime
|
|
||||||
function onTimeChanged() {
|
|
||||||
notificationTimeText.text = NotificationUtils.getFriendlyNotifTimeString(notificationObject.time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RippleButton { // Expand button
|
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
id: expandButton
|
|
||||||
implicitWidth: 22
|
|
||||||
implicitHeight: 22
|
|
||||||
|
|
||||||
buttonRadius: Appearance.rounding.full
|
|
||||||
colBackgroundHover: Appearance.colors.colLayer2Hover
|
|
||||||
colRipple: Appearance.colors.colLayer2Active
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
root.toggleExpanded()
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: MaterialSymbol {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "keyboard_arrow_down"
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
iconSize: Appearance.font.pixelSize.normal
|
|
||||||
color: Appearance.colors.colOnLayer2
|
|
||||||
rotation: expanded ? 180 : 0
|
|
||||||
Behavior on rotation {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Appearance.animation.elementMoveFast.duration
|
|
||||||
easing.type: Appearance.animation.elementMoveFast.type
|
|
||||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText { // Notification body
|
|
||||||
id: notificationBodyText
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.leftMargin: 10
|
|
||||||
Layout.rightMargin: 10
|
|
||||||
Layout.bottomMargin: 10
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
wrapMode: expanded ? Text.Wrap : Text.NoWrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
font.pixelSize: Appearance.font.pixelSize.small
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
color: Appearance.m3colors.m3outline
|
|
||||||
textFormat: expanded ? Text.RichText : Text.StyledText
|
|
||||||
text: expanded
|
|
||||||
? `<style>img{max-width:${notificationBodyText.width}px;}</style>` +
|
|
||||||
`${notificationObject.body.replace(/\n/g, "<br/>")}`
|
|
||||||
: notificationObject.body.replace(/<img/g, "\n <img").split("\n")[0]
|
|
||||||
onLinkActivated: (link) => {
|
|
||||||
Qt.openUrlExternally(link)
|
|
||||||
Hyprland.dispatch("global quickshell:sidebarRightClose")
|
|
||||||
}
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
acceptedButtons: Qt.NoButton // Only for hover
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
Flickable {
|
|
||||||
id: actionsFlickable
|
|
||||||
Layout.fillWidth: true
|
|
||||||
// Layout.topMargin: -5
|
|
||||||
Layout.leftMargin: 10
|
|
||||||
Layout.rightMargin: 10
|
|
||||||
Layout.bottomMargin: expanded ? 10 : 0
|
|
||||||
implicitHeight: expanded ? actionRowLayout.implicitHeight : 0
|
|
||||||
height: expanded ? actionRowLayout.implicitHeight : 0
|
|
||||||
contentWidth: actionRowLayout.implicitWidth
|
|
||||||
|
|
||||||
clip: true
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: OpacityMask {
|
|
||||||
maskSource: Rectangle {
|
|
||||||
width: actionsFlickable.width
|
|
||||||
height: actionsFlickable.height
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
opacity: expanded ? 1 : 0
|
|
||||||
visible: opacity > 0
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Appearance.animation.elementMoveFast.duration
|
|
||||||
easing.type: Appearance.animation.elementMoveFast.type
|
|
||||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Appearance.animation.elementMoveFast.duration
|
|
||||||
easing.type: Appearance.animation.elementMoveFast.type
|
|
||||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Appearance.animation.elementMoveFast.duration
|
|
||||||
easing.type: Appearance.animation.elementMoveFast.type
|
|
||||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: actionRowLayout
|
|
||||||
Layout.alignment: Qt.AlignBottom
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: actionRepeater
|
|
||||||
model: notificationObject.actions
|
|
||||||
NotificationActionButton {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
buttonText: modelData.text
|
|
||||||
urgency: notificationObject.urgency
|
|
||||||
onClicked: {
|
|
||||||
Notifications.attemptInvokeAction(notificationObject.id, modelData.identifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationActionButton {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
urgency: notificationObject.urgency
|
|
||||||
implicitWidth: (notificationObject.actions.length == 0) ? (actionsFlickable.width / 2) :
|
|
||||||
(contentItem.implicitWidth + leftPadding + rightPadding)
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(notificationObject.body)}'`)
|
|
||||||
copyIcon.text = "inventory"
|
|
||||||
copyIconTimer.restart()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: copyIconTimer
|
|
||||||
interval: 1500
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
copyIcon.text = "content_copy"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: MaterialSymbol {
|
|
||||||
id: copyIcon
|
|
||||||
iconSize: Appearance.font.pixelSize.large
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
|
|
||||||
Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
|
|
||||||
text: "content_copy"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationActionButton {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
buttonText: qsTr("Close")
|
|
||||||
urgency: notificationObject.urgency
|
|
||||||
implicitWidth: (notificationObject.actions.length == 0) ? (actionsFlickable.width / 2) :
|
|
||||||
(contentItem.implicitWidth + leftPadding + rightPadding)
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
root.destroyWithAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: MaterialSymbol {
|
|
||||||
iconSize: Appearance.font.pixelSize.large
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
|
|
||||||
Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface
|
|
||||||
text: "close"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import "root:/"
|
||||||
|
import "root:/modules/common/"
|
||||||
|
import "root:/modules/common/widgets"
|
||||||
|
import "root:/services"
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
|
||||||
|
ListView { // Scrollable window
|
||||||
|
id: root
|
||||||
|
spacing: 5
|
||||||
|
property real removeOvershoot: 20 // Account for gaps and bouncy animations
|
||||||
|
property int dragIndex: -1
|
||||||
|
property real dragDistance: 0
|
||||||
|
|
||||||
|
function resetDrag() {
|
||||||
|
root.dragIndex = -1
|
||||||
|
root.dragDistance = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
add: Transition {
|
||||||
|
animations: [
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
properties: "opacity,scale",
|
||||||
|
from: 0,
|
||||||
|
to: 1,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
addDisplaced: Transition {
|
||||||
|
animations: [
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
property: "y",
|
||||||
|
}),
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
properties: "opacity,scale",
|
||||||
|
to: 1,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
displaced: Transition {
|
||||||
|
animations: [
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
property: "y",
|
||||||
|
}),
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
properties: "opacity,scale",
|
||||||
|
to: 1,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
move: Transition {
|
||||||
|
animations: [
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
property: "y",
|
||||||
|
}),
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
properties: "opacity,scale",
|
||||||
|
to: 1,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
moveDisplaced: Transition {
|
||||||
|
animations: [
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
property: "y",
|
||||||
|
}),
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
properties: "opacity,scale",
|
||||||
|
to: 1,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
remove: Transition {
|
||||||
|
animations: [
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
property: "x",
|
||||||
|
to: root.width + root.removeOvershoot,
|
||||||
|
}),
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
property: "opacity",
|
||||||
|
to: 0,
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is movement when something is removed, not removing animation!
|
||||||
|
removeDisplaced: Transition {
|
||||||
|
animations: [
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
property: "y",
|
||||||
|
}),
|
||||||
|
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
||||||
|
properties: "opacity,scale",
|
||||||
|
to: 1,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,74 +34,14 @@ Scope {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
implicitWidth: Appearance.sizes.notificationPopupWidth
|
implicitWidth: Appearance.sizes.notificationPopupWidth
|
||||||
|
|
||||||
ListView { // Scrollable window
|
NotificationListView {
|
||||||
id: listview
|
id: listview
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.topMargin: 5
|
||||||
implicitWidth: parent.width - Appearance.sizes.elevationMargin * 2
|
implicitWidth: parent.width - Appearance.sizes.elevationMargin * 2
|
||||||
|
popup: true
|
||||||
add: Transition {
|
|
||||||
animations: [
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
properties: "opacity,scale",
|
|
||||||
from: 0,
|
|
||||||
to: 1,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
addDisplaced: Transition {
|
|
||||||
animations: [
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
property: "y",
|
|
||||||
}),
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
properties: "opacity,scale",
|
|
||||||
to: 1,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
displaced: Transition {
|
|
||||||
animations: [
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
property: "y",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
move: Transition {
|
|
||||||
animations: [
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
property: "y",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
remove: Transition {
|
|
||||||
animations: [
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
property: "x",
|
|
||||||
to: listview.width,
|
|
||||||
}),
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
property: "opacity",
|
|
||||||
to: 0,
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
model: ScriptModel {
|
|
||||||
values: Notifications.popupList.slice().reverse()
|
|
||||||
}
|
|
||||||
delegate: NotificationWidget {
|
|
||||||
required property var modelData
|
|
||||||
id: notificationWidget
|
|
||||||
popup: true
|
|
||||||
anchors.left: parent?.left
|
|
||||||
anchors.right: parent?.right
|
|
||||||
notificationObject: modelData
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Quickshell.Widgets
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
ListView { // Scrollable window
|
NotificationListView { // Scrollable window
|
||||||
id: listview
|
id: listview
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
@@ -28,68 +28,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add: Transition {
|
popup: false
|
||||||
animations: [
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
properties: "opacity,scale",
|
|
||||||
from: 0,
|
|
||||||
to: 1,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
addDisplaced: Transition {
|
|
||||||
animations: [
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
property: "y",
|
|
||||||
}),
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
properties: "opacity,scale",
|
|
||||||
to: 1,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
displaced: Transition {
|
|
||||||
animations: [
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
property: "y",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
move: Transition {
|
|
||||||
animations: [
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
property: "y",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
remove: Transition {
|
|
||||||
animations: [
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
property: "x",
|
|
||||||
to: listview.width,
|
|
||||||
}),
|
|
||||||
Appearance.animation.elementMove.numberAnimation.createObject(this, {
|
|
||||||
property: "opacity",
|
|
||||||
to: 0,
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
model: ScriptModel {
|
|
||||||
values: Notifications.list.slice().reverse()
|
|
||||||
}
|
|
||||||
delegate: NotificationWidget {
|
|
||||||
required property var modelData
|
|
||||||
id: notificationWidget
|
|
||||||
// anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.left: parent?.left
|
|
||||||
anchors.right: parent?.right
|
|
||||||
Layout.fillWidth: true
|
|
||||||
notificationObject: modelData
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder when list is empty
|
// Placeholder when list is empty
|
||||||
|
|||||||
Reference in New Issue
Block a user