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).
|
||||
* @returns {Qt.rgba} The resulting color.
|
||||
*/
|
||||
function transparentize(color, percentage) {
|
||||
function transparentize(color, percentage = 1) {
|
||||
var c = Qt.color(color);
|
||||
return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ MouseArea { // Flick to dismiss
|
||||
|
||||
onPressed: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
mouse.accepted = false;
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
mouse.accepted = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
@@ -40,7 +42,6 @@ MouseArea { // Flick to dismiss
|
||||
}
|
||||
onReleased: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
mouse.accepted = false;
|
||||
return;
|
||||
}
|
||||
dragging = false
|
||||
@@ -51,7 +52,6 @@ MouseArea { // Flick to dismiss
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
mouse.accepted = false;
|
||||
return;
|
||||
}
|
||||
if (mouse.buttons & Qt.LeftButton) {
|
||||
@@ -64,7 +64,6 @@ MouseArea { // Flick to dismiss
|
||||
}
|
||||
onCanceled: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
mouse.accepted = false;
|
||||
return;
|
||||
}
|
||||
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"
|
||||
implicitWidth: Appearance.sizes.notificationPopupWidth
|
||||
|
||||
ListView { // Scrollable window
|
||||
NotificationListView {
|
||||
id: listview
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: 5
|
||||
implicitWidth: parent.width - Appearance.sizes.elevationMargin * 2
|
||||
|
||||
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
|
||||
}
|
||||
popup: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import Quickshell.Widgets
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ListView { // Scrollable window
|
||||
NotificationListView { // Scrollable window
|
||||
id: listview
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
@@ -28,68 +28,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
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.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
|
||||
}
|
||||
popup: false
|
||||
}
|
||||
|
||||
// Placeholder when list is empty
|
||||
|
||||
Reference in New Issue
Block a user