Merge pull request #344 from FUFSoB/notifications-refine

Notifications improvements
This commit is contained in:
Lemmy
2025-09-23 23:01:33 -04:00
committed by GitHub
5 changed files with 83 additions and 23 deletions
+3 -1
View File
@@ -127,7 +127,9 @@
"doNotDisturb": false,
"monitors": [],
"location": "top_right",
"alwaysOnTop": false,
"lastSeenTs": 0,
"respectExpireTimeout": false,
"lowUrgencyDuration": 3,
"normalUrgencyDuration": 8,
"criticalUrgencyDuration": 15,
@@ -182,4 +184,4 @@
"wallpaperChange": "",
"darkModeChange": ""
}
}
}
+2
View File
@@ -249,7 +249,9 @@ Singleton {
property bool doNotDisturb: false
property list<string> monitors: []
property string location: "top_right"
property bool alwaysOnTop: false
property real lastSeenTs: 0
property bool respectExpireTimeout: false
property int lowUrgencyDuration: 3
property int normalUrgencyDuration: 8
property int criticalUrgencyDuration: 15
+31 -6
View File
@@ -37,6 +37,10 @@ Variants {
sourceComponent: PanelWindow {
screen: modelData
WlrLayershell.namespace: "noctalia-notifications"
WlrLayershell.layer: (Settings.isLoaded && Settings.data && Settings.data.notifications && Settings.data.notifications.alwaysOnTop) ? WlrLayer.Overlay : WlrLayer.Top
color: Color.transparent
readonly property string location: (Settings.isLoaded && Settings.data && Settings.data.notifications && Settings.data.notifications.location) ? Settings.data.notifications.location : "top_right"
@@ -103,7 +107,7 @@ Variants {
// Connect to animation signal from service - UPDATED TO USE ID
Component.onCompleted: {
NotificationService.animateAndRemove.connect(function (notificationId, index) {
NotificationService.animateAndRemove.connect(function (notificationId) {
// Find the delegate by notification ID
var delegate = null
if (notificationStack && notificationStack.children && notificationStack.children.length > 0) {
@@ -116,11 +120,6 @@ Variants {
}
}
// Fallback to index if ID lookup failed
if (!delegate && notificationStack && notificationStack.children && notificationStack.children[index]) {
delegate = notificationStack.children[index]
}
if (delegate && delegate.animateOut) {
delegate.animateOut()
} else {
@@ -159,6 +158,32 @@ Variants {
border.width: Math.max(1, Style.borderS * scaling)
color: Color.mSurface
Rectangle {
id: progressBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 2 * scaling
color: "transparent"
property real availableWidth: parent.width - (2 * parent.radius)
Rectangle {
x: parent.parent.radius + (parent.availableWidth * (1 - model.progress)) / 2
width: parent.availableWidth * model.progress
height: parent.height
color: {
if (model.urgency === NotificationUrgency.Critical || model.urgency === 2)
return Color.mError
else if (model.urgency === NotificationUrgency.Low || model.urgency === 0)
return Color.mOnSurface
else
return Color.mPrimary
}
antialiasing: true
}
}
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
@@ -78,6 +78,13 @@ ColumnLayout {
currentKey: Settings.data.notifications.location || "top_right"
onSelected: key => Settings.data.notifications.location = key
}
NToggle {
label: "Always on top"
description: "Display notifications above fullscreen windows and other layers."
checked: Settings.data.notifications.alwaysOnTop
onToggled: checked => Settings.data.notifications.alwaysOnTop = checked
}
}
NDivider {
@@ -96,6 +103,14 @@ ColumnLayout {
description: "Configure how long notifications stay visible based on their urgency level."
}
// Respect Expire Timeout (eg. --expire-time flag in notify-send)
NToggle {
label: "Respect expire timeout"
description: "Use the expire timeout set in the notification."
checked: Settings.data.notifications.respectExpireTimeout
onToggled: checked => Settings.data.notifications.respectExpireTimeout = checked
}
// Low Urgency Duration
ColumnLayout {
spacing: Style.marginXXS * scaling
+32 -16
View File
@@ -24,6 +24,7 @@ Singleton {
// Internal state
property var activeMap: ({})
property var imageQueue: []
property var progressTimers: ({})
// Simple image cacher
PanelWindow {
@@ -117,8 +118,10 @@ Singleton {
"summary": (n.summary || ""),
"body": stripTags(n.body || ""),
"appName": getAppName(n.appName),
"urgency": n.urgency || 1,
"urgency": n.urgency < 0 || n.urgency > 2 ? 1 : n.urgency,
"expireTimeout": n.expireTimeout,
"timestamp": time,
"progress": 1.0,
"originalImage": image,
"cachedImage": imageId ? (Settings.cacheDirImagesNotifications + imageId + ".png") : image,
"actionsJson": JSON.stringify((n.actions || []).map(a => ({
@@ -160,7 +163,6 @@ Singleton {
function updateModel(model, id, prop, value) {
for (var i = 0; i < model.count; i++) {
if (model.get(i).id === id) {
model.setProperty(i, prop, "")
model.setProperty(i, prop, value)
break
}
@@ -172,6 +174,7 @@ Singleton {
if (activeList.get(i).id === id) {
activeList.remove(i)
delete activeMap[id]
delete progressTimers[id]
break
}
}
@@ -179,19 +182,31 @@ Singleton {
// Auto-hide timer
Timer {
interval: 1000
interval: 10
repeat: true
running: activeList.count > 0
onTriggered: {
const now = Date.now()
const durations = [3000, 8000, 15000] // low, normal, critical
const durations = [Settings.data.notifications?.lowUrgencyDuration * 1000 || 3000,
Settings.data.notifications?.normalUrgencyDuration * 1000 || 8000,
Settings.data.notifications?.criticalUrgencyDuration * 1000 || 15000]
for (var i = activeList.count - 1; i >= 0; i--) {
const notif = activeList.get(i)
const elapsed = now - notif.timestamp.getTime()
var expire = 0
if (elapsed >= durations[notif.urgency] || elapsed >= 8000) {
animateAndRemove(notif.id, i)
if (Settings.data.notifications?.respectExpireTimeout)
expire = notif.expireTimeout > 0 ? notif.expireTimeout : durations[notif.urgency]
else
expire = durations[notif.urgency]
const progress = Math.max(1.0 - (elapsed / expire), 0.0)
updateModel(activeList, notif.id, "progress", progress)
if (elapsed >= expire) {
animateAndRemove(notif.id)
delete progressTimers[notif.id]
break
}
}
@@ -273,21 +288,22 @@ Singleton {
}
historyList.append({
"id": item.id || "",
"summary": item.summary || "",
"body": item.body || "",
"appName": item.appName || "",
"urgency": item.urgency || 1,
"timestamp": time,
"originalImage": item.originalImage || "",
"cachedImage": cachedImage
})
"id": item.id || "",
"summary": item.summary || "",
"body": item.body || "",
"appName": item.appName || "",
"urgency": item.urgency < 0 || item.urgency > 2 ? 1 : item.urgency,
"timestamp": time,
"originalImage": item.originalImage || "",
"cachedImage": cachedImage
})
}
} catch (e) {
Logger.error("Notifications", "Load failed:", e)
}
}
// Helpers
function getAppName(name) {
if (!name?.includes("."))
@@ -380,7 +396,7 @@ Singleton {
}
// Signals & connections
signal animateAndRemove(string notificationId, int index)
signal animateAndRemove(string notificationId)
Connections {
target: Settings.data.notifications