Notifications: minor renaming + house keeping. Bring back the close history when clearing all notifications

This commit is contained in:
LemmyCook
2025-09-21 10:24:47 -04:00
parent fac9b8f54c
commit 0279b5654a
5 changed files with 114 additions and 110 deletions
+61 -49
View File
@@ -8,6 +8,7 @@ import qs.Services
Singleton {
id: root
// Current date
property var date: new Date()
// Returns a Unix Timestamp (in seconds)
@@ -15,62 +16,73 @@ Singleton {
return Math.floor(date / 1000)
}
/**
* Formats a Date object into a YYYYMMDD-HHMMSS string.
* @param {Date} [date=new Date()] - The date to format. Defaults to the current date and time.
* @returns {string} The formatted date string.
*/
function getFormattedTimestamp(date = new Date()) {
const year = date.getFullYear()
// getMonth() is zero-based, so we add 1
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}${month}${day}-${hours}${minutes}${seconds}`
}
// Format an easy to read approximate duration ex: 4h32m
// Used to display the time remaining on the Battery widget, computer uptime, etc..
function formatVagueHumanReadableDuration(totalSeconds) {
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
return '0s'
Timer {
interval: 1000
repeat: true
running: true
onTriggered: root.date = new Date()
}
// Floor the input to handle decimal seconds
totalSeconds = Math.floor(totalSeconds)
// Formats a Date object into a YYYYMMDD-HHMMSS string.
function getFormattedTimestamp(date) {
if (!date) {
date = new Date()
}
const year = date.getFullYear()
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
// getMonth() is zero-based, so we add 1
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const parts = []
if (days)
parts.push(`${days}d`)
if (hours)
parts.push(`${hours}h`)
if (minutes)
parts.push(`${minutes}m`)
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
// Only show seconds if no hours and no minutes
if (!hours && !minutes) {
parts.push(`${seconds}s`)
return `${year}${month}${day}-${hours}${minutes}${seconds}`
}
return parts.join('')
}
// Format an easy to read approximate duration ex: 4h32m
// Used to display the time remaining on the Battery widget, computer uptime, etc..
function formatVagueHumanReadableDuration(totalSeconds) {
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
return '0s'
}
Timer {
interval: 1000
repeat: true
running: true
// Floor the input to handle decimal seconds
totalSeconds = Math.floor(totalSeconds)
onTriggered: root.date = new Date()
}
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const parts = []
if (days)
parts.push(`${days}d`)
if (hours)
parts.push(`${hours}h`)
if (minutes)
parts.push(`${minutes}m`)
// Only show seconds if no hours and no minutes
if (!hours && !minutes) {
parts.push(`${seconds}s`)
}
return parts.join('')
}
// Format a date into
function formatRelativeTime(date) {
if (!date)
return ""
const diff = Date.now() - date.getTime()
if (diff < 60000)
return "now"
if (diff < 3600000)
return `${Math.floor(diff / 60000)}m ago`
if (diff < 86400000)
return `${Math.floor(diff / 3600000)}h ago`
return `${Math.floor(diff / 86400000)}d ago`
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ NIconButton {
function computeUnreadCount() {
var since = lastSeenTs()
var count = 0
var model = NotificationService.notificationHistory
var model = NotificationService.historyList
for (var i = 0; i < model.count; i++) {
var item = model.get(i)
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp
+2 -2
View File
@@ -19,7 +19,7 @@ Variants {
readonly property real scaling: ScalingService.getScreenScale(modelData)
// Access the notification model from the service - UPDATED NAME
property ListModel notificationModel: NotificationService.activeNotifications
property ListModel notificationModel: NotificationService.activeList
// If no notification display activated in settings, then show them all
active: Settings.isLoaded && modelData && (notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0)) : false
@@ -256,7 +256,7 @@ Variants {
}
NText {
text: `${model.appName || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
text: `${model.appName || "Unknown App"} · ${Time.formatRelativeTime(model.timestamp)}`
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
@@ -55,7 +55,11 @@ NPanel {
icon: "trash"
tooltipText: "Clear history"
baseSize: Style.baseWidgetSize * 0.8
onClicked: NotificationService.clearHistory()
onClicked: {
NotificationService.clearHistory()
// Close panel as there is nothing more to see.
root.close()
}
}
NIconButton {
@@ -75,7 +79,7 @@ NPanel {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: NotificationService.notificationHistory.count === 0
visible: NotificationService.historyList.count === 0
spacing: Style.marginL * scaling
Item {
@@ -119,11 +123,11 @@ NPanel {
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
model: NotificationService.notificationHistory
model: NotificationService.historyList
spacing: Style.marginM * scaling
clip: true
boundsBehavior: Flickable.StopAtBounds
visible: NotificationService.notificationHistory.count > 0
visible: NotificationService.historyList.count > 0
delegate: Rectangle {
property string notificationId: model.id
@@ -200,7 +204,7 @@ NPanel {
}
NText {
text: NotificationService.formatTimestamp(model.timestamp)
text: Time.formatRelativeTime(model.timestamp)
font.pointSize: Style.fontSizeXS * scaling
color: Color.mSecondary
}
+41 -53
View File
@@ -18,8 +18,8 @@ Singleton {
property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json")
// Models
property ListModel activeNotifications: ListModel {}
property ListModel notificationHistory: ListModel {}
property ListModel activeList: ListModel {}
property ListModel historyList: ListModel {}
// Internal state
property var activeMap: ({})
@@ -39,6 +39,8 @@ Singleton {
visible: true
cache: false
asynchronous: true
mipmap: true
antialiasing: true
onStatusChanged: {
if (imageQueue.length === 0)
@@ -88,11 +90,11 @@ Singleton {
notification.tracked = true
notification.closed.connect(() => removeActive(data.id))
activeNotifications.insert(0, data)
while (activeNotifications.count > maxVisible) {
const last = activeNotifications.get(activeNotifications.count - 1)
activeList.insert(0, data)
while (activeList.count > maxVisible) {
const last = activeList.get(activeList.count - 1)
activeMap[last.id]?.dismiss()
activeNotifications.remove(activeNotifications.count - 1)
activeList.remove(activeList.count - 1)
}
}
@@ -147,8 +149,8 @@ Singleton {
}
function updateImagePath(id, path) {
updateModel(activeNotifications, id, "cachedImage", path)
updateModel(notificationHistory, id, "cachedImage", path)
updateModel(activeList, id, "cachedImage", path)
updateModel(historyList, id, "cachedImage", path)
saveHistory()
}
@@ -162,9 +164,9 @@ Singleton {
}
function removeActive(id) {
for (var i = 0; i < activeNotifications.count; i++) {
if (activeNotifications.get(i).id === id) {
activeNotifications.remove(i)
for (var i = 0; i < activeList.count; i++) {
if (activeList.get(i).id === id) {
activeList.remove(i)
delete activeMap[id]
break
}
@@ -175,13 +177,13 @@ Singleton {
Timer {
interval: 1000
repeat: true
running: activeNotifications.count > 0
running: activeList.count > 0
onTriggered: {
const now = Date.now()
const durations = [3000, 8000, 15000] // low, normal, critical
for (var i = activeNotifications.count - 1; i >= 0; i--) {
const notif = activeNotifications.get(i)
for (var i = activeList.count - 1; i >= 0; i--) {
const notif = activeList.get(i)
const elapsed = now - notif.timestamp.getTime()
if (elapsed >= durations[notif.urgency] || elapsed >= 8000) {
@@ -194,16 +196,15 @@ Singleton {
// History management
function addToHistory(data) {
notificationHistory.insert(0, data)
historyList.insert(0, data)
while (notificationHistory.count > maxHistory) {
const old = notificationHistory.get(notificationHistory.count - 1)
while (historyList.count > maxHistory) {
const old = historyList.get(historyList.count - 1)
if (old.cachedImage && !old.cachedImage.startsWith("image://")) {
Quickshell.execDetached(["rm", "-f", old.cachedImage])
}
notificationHistory.remove(notificationHistory.count - 1)
historyList.remove(historyList.count - 1)
}
saveHistory()
}
@@ -215,7 +216,7 @@ Singleton {
onLoaded: loadHistory()
onLoadFailed: error => {
if (error === 2)
writeAdapter()
writeAdapter()
}
JsonAdapter {
@@ -237,8 +238,8 @@ Singleton {
function performSave() {
try {
const items = []
for (var i = 0; i < notificationHistory.count; i++) {
const n = notificationHistory.get(i)
for (var i = 0; i < historyList.count; i++) {
const n = historyList.get(i)
const copy = Object.assign({}, n)
copy.timestamp = n.timestamp.getTime()
items.push(copy)
@@ -253,7 +254,7 @@ Singleton {
function loadHistory() {
try {
notificationHistory.clear()
historyList.clear()
for (const item of adapter.notifications || []) {
let time = item.timestamp
if (typeof time === "number") {
@@ -273,17 +274,17 @@ Singleton {
cachedImage = Settings.cacheDirImagesNotifications + imageId + ".png"
}
}
notificationHistory.append({
"id": item.id || "",
"summary": item.summary || "",
"body": item.body || "",
"appName": item.appName || "",
"urgency": item.urgency || 1,
"timestamp": time,
"originalImage": item.originalImage || "",
"cachedImage": cachedImage
})
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
})
}
} catch (e) {
Logger.error("Notifications", "Load failed:", e)
@@ -337,7 +338,7 @@ Singleton {
function dismissAllActive() {
Object.values(activeMap).forEach(n => n.dismiss())
activeNotifications.clear()
activeList.clear()
activeMap = {}
}
@@ -356,14 +357,14 @@ Singleton {
}
function removeFromHistory(notificationId) {
for (let i = 0; i < notificationHistory.count; i++) {
const notif = notificationHistory.get(i)
for (var i = 0; i < historyList.count; i++) {
const notif = historyList.get(i)
if (notif.id === notificationId) {
// Delete cached image if it exists
if (notif.cachedImage && !notif.cachedImage.startsWith("image://")) {
Quickshell.execDetached(["rm", "-f", notif.cachedImage])
}
notificationHistory.remove(i)
historyList.remove(i)
saveHistory()
return true
}
@@ -378,22 +379,9 @@ Singleton {
} catch (e) {
Logger.error("Notifications", "Failed to clear cache directory:", e)
}
notificationHistory.clear()
saveHistory()
}
function formatTimestamp(timestamp) {
if (!timestamp)
return ""
const diff = Date.now() - timestamp.getTime()
if (diff < 60000)
return "now"
if (diff < 3600000)
return `${Math.floor(diff / 60000)}m ago`
if (diff < 86400000)
return `${Math.floor(diff / 3600000)}h ago`
return `${Math.floor(diff / 86400000)}d ago`
historyList.clear()
saveHistory()
}
// Signals & connections