pragma Singleton pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Notifications import Qt.labs.platform Singleton { id: root property var filePath: `${StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]}/notifications/notifications.json` property var list: [] // Quickshell's notification IDs starts at 1 on each run, while saved notifications // can already contain higher IDs. This is a workaround to avoid id collisions property int idOffset signal initDone(); signal notify(notification: var); signal discard(id: var); signal discardAll(); signal timeout(id: var); NotificationServer { id: notifServer // actionIconsSupported: true actionsSupported: true bodyHyperlinksSupported: true bodyImagesSupported: true bodyMarkupSupported: true bodySupported: true imageSupported: true keepOnReload: false persistenceSupported: true onNotification: (notification) => { notification.tracked = true const newNotifObject = { "id": notification.id + root.idOffset, "actions": notification.actions.map((action) => { return { "identifier": action.identifier, "text": action.text, } }), "appIcon": notification.appIcon, "appName": notification.appName, "body": notification.body, "image": notification.image, "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 + root.idOffset === 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 discardAllNotifications() { root.list = [] triggerListChange() notifFileView.setText(JSON.stringify(root.list, null, 2)) notifServer.trackedNotifications.values.forEach((notif) => { notif.dismiss() }) root.discardAll(); } function timeoutNotification(id) { root.timeout(id); } function timeoutAll() { root.list.forEach((notif) => { root.timeout(notif.id); }) } function attemptInvokeAction(id, notifIdentifier) { const notifServerIndex = notifServer.trackedNotifications.values.findIndex((notif) => notif.id + root.idOffset === id); if (notifServerIndex !== -1) { const notifServerNotif = notifServer.trackedNotifications.values[notifServerIndex]; const action = notifServerNotif.actions.find((action) => action.identifier === notifIdentifier); action.invoke() } // else console.log("Notification not found in server: " + id) 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) // Find largest id let maxId = 0 root.list.forEach((notif) => { maxId = Math.max(maxId, notif.id) }) console.log("[Notifications] File loaded") root.idOffset = maxId 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) } } } }