notifications properly working

This commit is contained in:
end-4
2025-04-19 20:07:44 +02:00
parent 63e29d18fb
commit 3b2628fbd7
6 changed files with 288 additions and 119 deletions
@@ -8,129 +8,201 @@ import Quickshell.Widgets
import Quickshell.Services.Notifications
import "./notification_utils.js" as NotificationUtils
WrapperRectangle {
Item {
id: root
property var notificationObject
property bool expanded: true
property bool expanded: false
property bool enableAnimation: true
property int notificationListSpacing: 5
property bool ready: false
Layout.fillWidth: true
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
Appearance.m3colors.m3secondaryContainer : Appearance.colors.colLayer2
radius: Appearance.rounding.normal
RowLayout {
clip: true
// implicitHeight: notificationRowLayout.implicitHeight
implicitHeight: ready ? notificationRowLayout.implicitHeight + notificationListSpacing : 0
Behavior on implicitHeight {
enabled: enableAnimation
NumberAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
Component.onCompleted: {
root.ready = true
}
function fancyDestroy() {
implicitHeight = 0
notificationRowWrapper.anchors.top = undefined
notificationRowWrapper.anchors.bottom = root.bottom
destroyTimer.start()
}
Timer {
id: destroyTimer
interval: Appearance.animation.elementDecel.duration
repeat: false
onTriggered: {
root.destroy()
}
}
MouseArea { // Middle click to close
anchors.fill: parent
Rectangle {
id: iconRectangle
implicitWidth: 47
implicitHeight: 47
Layout.leftMargin: 10
Layout.topMargin: 10
Layout.bottomMargin: 10
Layout.alignment: Qt.AlignTop
radius: Appearance.rounding.full
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
Appearance.m3colors.m3secondary : Appearance.m3colors.m3secondaryContainer
MaterialSymbol {
visible: notificationObject.appIcon == ""
text: NotificationUtils.guessMessageType(notificationObject.summary)
anchors.fill: parent
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
Appearance.m3colors.m3onSecondary : Appearance.m3colors.m3onSecondaryContainer
font.pixelSize: 27
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
IconImage {
visible: notificationObject.appIcon != ""
anchors.centerIn: parent
implicitSize: 33
asynchronous: true
source: Quickshell.iconPath(notificationObject.appIcon)
acceptedButtons: Qt.MiddleButton
onClicked: (mouse) => {
if (mouse.button == Qt.MiddleButton) {
Notifications.discardNotification(notificationObject.id)
}
}
ColumnLayout {
spacing: 0
RowLayout {
Layout.topMargin: 10
Layout.leftMargin: 10
Layout.rightMargin: 10
Layout.fillWidth: true
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer2
text: notificationObject.summary
wrapMode: expanded ? Text.Wrap : Text.NoWrap
elide: Text.ElideRight
}
Item { Layout.fillWidth: true }
StyledText {
id: notificationTimeText
Layout.fillWidth: false
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignLeft
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.m3colors.m3outline
text: NotificationUtils.getFriendlyNotifTimeString(notificationObject.time)
}
Connections {
target: DateTime
function onTimeChanged() {
notificationTimeText.text = NotificationUtils.getFriendlyNotifTimeString(notificationObject.time)
// Background
Rectangle {
anchors.fill: parent
anchors.topMargin: notificationListSpacing
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
Appearance.m3colors.m3secondaryContainer : Appearance.colors.colLayer2
radius: Appearance.rounding.normal
}
Item {
id: notificationRowWrapper
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: notificationRowLayout.implicitHeight + notificationListSpacing
RowLayout {
id: notificationRowLayout
anchors.left: parent.left
anchors.right: parent.right
// anchors.top: parent.top
anchors.bottom: parent.bottom
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: (notificationObject.urgency == NotificationUrgency.Critical) ?
Appearance.m3colors.m3secondary : Appearance.m3colors.m3secondaryContainer
MaterialSymbol {
visible: notificationObject.appIcon == ""
text: NotificationUtils.guessMessageType(notificationObject.summary)
anchors.fill: parent
color: (notificationObject.urgency == NotificationUrgency.Critical) ?
Appearance.m3colors.m3onSecondary : Appearance.m3colors.m3onSecondaryContainer
font.pixelSize: 27
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
IconImage {
visible: notificationObject.appIcon != ""
anchors.centerIn: parent
implicitSize: 33
asynchronous: true
source: Quickshell.iconPath(notificationObject.appIcon)
}
}
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
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer2
text: notificationObject.summary
wrapMode: expanded ? Text.Wrap : Text.NoWrap
elide: Text.ElideRight
}
Item { Layout.fillWidth: true }
StyledText { // Time
id: notificationTimeText
Layout.fillWidth: false
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)
}
}
}
}
Button {
Layout.alignment: Qt.AlignVCenter
id: expandButton
implicitWidth: 22
implicitHeight: 22
onClicked: {
root.expanded = !root.expanded
}
PointingHandInteraction{}
Button { // Expand button
Layout.alignment: Qt.AlignVCenter
id: expandButton
implicitWidth: 22
implicitHeight: 22
background: Rectangle {
anchors.fill: parent
radius: Appearance.rounding.full
color: (expandButton.down) ? Appearance.colors.colLayer2Active : (expandButton.hovered ? Appearance.colors.colLayer2Hover : Appearance.transparentize(Appearance.colors.colLayer2, 1))
PointingHandInteraction{}
onClicked: {
root.enableAnimation = true
root.expanded = !root.expanded
}
background: Rectangle {
anchors.fill: parent
radius: Appearance.rounding.full
color: (expandButton.down) ? Appearance.colors.colLayer2Active : (expandButton.hovered ? Appearance.colors.colLayer2Hover : Appearance.transparentize(Appearance.colors.colLayer2, 1))
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: expanded ? "keyboard_arrow_up" : "keyboard_arrow_down"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer2
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: expanded ? "keyboard_arrow_up" : "keyboard_arrow_down"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer2
}
}
}
}
RowLayout {
StyledText {
StyledText { // Notification body
Layout.fillWidth: true
Layout.bottomMargin: 10
Layout.leftMargin: 10
Layout.rightMargin: 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: Text.MarkdownText
text: notificationObject.body
// textFormat: Text.MarkdownText
text: notificationObject.body
}
}
}
@@ -42,8 +42,8 @@ function guessMessageType(summary) {
// return messageTime.format(userOptions.time.dateFormat);
// }
const getFriendlyNotifTimeString = (timeObject) => {
const messageTime = timeObject;
const getFriendlyNotifTimeString = (timestamp) => {
const messageTime = new Date(timestamp);
const now = new Date();
const oneMinuteAgo = new Date(now.getTime() - 60000);
@@ -7,6 +7,50 @@ import QtQuick.Layouts
import Quickshell.Widgets
Item {
id: root
property Component notifComponent: NotificationWidget {}
property list<NotificationWidget> notificationWidgetList: []
// Signal handlers to add/remove notifications
Connections {
target: Notifications
function onInitDone() {
// notificationRepeater.model = Notifications.list.slice().reverse()
Notifications.list.slice().reverse().forEach((notification) => {
const notif = root.notifComponent.createObject(columnLayout, { notificationObject: notification });
notificationWidgetList.push(notif)
})
}
function onNotify(notification) {
// notificationRepeater.model = [notification, ...notificationRepeater.model]
const notif = root.notifComponent.createObject(columnLayout, { notificationObject: notification });
notificationWidgetList.unshift(notif)
// Remove stuff from t he column, add back
for (let i = 0; i < notificationWidgetList.length; i++) {
if (notificationWidgetList[i].parent === columnLayout) {
notificationWidgetList[i].parent = null;
}
}
// Add notification widgets to the column
for (let i = 0; i < notificationWidgetList.length; i++) {
if (notificationWidgetList[i].parent === null) {
notificationWidgetList[i].parent = columnLayout;
}
}
}
function onDiscard(id) {
for (let i = notificationWidgetList.length - 1; i >= 0; i--) {
const widget = notificationWidgetList[i];
if (widget && widget.notificationObject && widget.notificationObject.id === id) {
widget.fancyDestroy();
notificationWidgetList.splice(i, 1);
}
}
}
}
Flickable { // Scrollable window
id: flickable
anchors.fill: parent
@@ -14,20 +58,12 @@ Item {
clip: true
ColumnLayout { // Scrollable window content
id: columnLayout
anchors.left: parent.left
anchors.right: parent.right
id: columnLayout
Repeater {
model: Notifications.list
delegate: NotificationWidget {
notificationObject: modelData
}
}
spacing: 0 // The widgets themselves have margins for spacing
// Notifications are added by the above signal handlers
}
}
}
@@ -94,7 +94,6 @@ Item {
Item {
Layout.fillWidth: true
}
// layoutDirection: Qt.RightToLeft
TodoItemActionButton {
Layout.fillWidth: false
onClicked: {
+72 -10
View File
@@ -3,30 +3,92 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Notifications
import Qt.labs.platform
Singleton {
id: root
property alias list: notifServer.trackedNotifications
property var filePath: `${StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]}/notifications/notifications.json`
property var list: []
signal initDone();
signal notify(notification: var);
signal discard(id: var);
NotificationServer {
id: notifServer
actionIconsSupported: true
actionsSupported: true
// actionIconsSupported: true
// actionsSupported: true
bodyHyperlinksSupported: true
bodyImagesSupported: true
// bodyImagesSupported: true
bodyMarkupSupported: true
bodySupported: true
imageSupported: true
keepOnReload: true
// imageSupported: true
keepOnReload: false // I can't figure out RetainableLock, using a custom solution with a json file instead
persistenceSupported: true
onNotification: (notification) => {
notification.tracked = true;
if(!notification.time) {
notification.time = new Date();
notification.tracked = true
const newNotifObject = {
"id": notification.id,
"actions": [],
"appIcon": notification.appIcon,
"appName": notification.appName,
"body": notification.body,
"summary": notification.summary,
"time": Date.now(),
"urgency": notification.urgency.toString(),
}
root.list = [...root.list, newNotifObject];
root.notify(newNotifObject);
notifFileView.setText(JSON.stringify(root.list, null, 2))
}
}
function discardNotification(id) {
const index = root.list.findIndex((notif) => notif.id === id);
const notifServerIndex = notifServer.trackedNotifications.values.findIndex((notif) => notif.id === id);
if (index !== -1) {
root.list.splice(index, 1);
notifFileView.setText(JSON.stringify(root.list, null, 2))
triggerListChange()
}
if (notifServerIndex !== -1) {
notifServer.trackedNotifications.values[notifServerIndex].dismiss()
}
root.discard(id);
}
function triggerListChange() {
root.list = root.list.slice(0)
}
function refresh() {
notifFileView.reload()
}
Component.onCompleted: {
refresh()
}
FileView {
id: notifFileView
path: filePath
onLoaded: {
const fileContents = notifFileView.text()
root.list = JSON.parse(fileContents)
console.log("[Notifications] File loaded")
root.initDone()
}
onLoadFailed: (error) => {
if(error == FileViewError.FileNotFound) {
console.log("[Notifications] File not found, creating new file.")
root.list = []
notifFileView.setText(JSON.stringify(root.list))
} else {
console.log("[Notifications] Error loading file: " + error)
}
// root.list = [...root.list, notification];
}
}
}
+1 -1
View File
@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import Quickshell;
import Quickshell.Io;
import Quickshell.Services.Pipewire;
import Qt.labs.platform
import QtQuick;
@@ -68,6 +67,7 @@ Singleton {
onLoaded: {
const fileContents = todoFileView.text()
root.list = JSON.parse(fileContents)
console.log("[To Do] File loaded")
}
onLoadFailed: (error) => {
if(error == FileViewError.FileNotFound) {