Files
illogical-impulse/.config/quickshell/services/Notifications.qml
T
2025-05-25 23:36:18 +02:00

202 lines
6.8 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import "root:/modules/common"
import "root:/"
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Notifications
import Qt.labs.platform
Singleton {
id: root
property var filePath: `${XdgDirectories.cache}/notifications/notifications.json`
property var list: []
property var popupList: []
property bool popupInhibited: GlobalStates?.sidebarRightOpen ?? false
property var latestTimeForApp: ({})
onListChanged: {
// Update latest time for each app
root.list.forEach((notif) => {
if (!root.latestTimeForApp[notif.appName] || notif.time > root.latestTimeForApp[notif.appName]) {
root.latestTimeForApp[notif.appName] = Math.max(root.latestTimeForApp[notif.appName] || 0, notif.time);
}
});
// Remove apps that no longer have notifications
Object.keys(root.latestTimeForApp).forEach((appName) => {
if (!root.list.some((notif) => notif.appName === appName)) {
delete root.latestTimeForApp[appName];
}
});
}
function appNameListForGroups(groups) {
return Object.keys(groups).sort((a, b) => {
// Sort by time, descending
return groups[b].time - groups[a].time;
});
}
function groupsForList(list) {
const groups = {};
list.forEach((notif) => {
if (!groups[notif.appName]) {
groups[notif.appName] = {
appName: notif.appName,
appIcon: notif.appIcon,
notifications: [],
time: 0
};
}
groups[notif.appName].notifications.push(notif);
// Always set to the latest time in the group
groups[notif.appName].time = latestTimeForApp[notif.appName] || notif.time;
});
return groups;
}
property var groupsByAppName: groupsForList(root.list)
property var popupGroupsByAppName: groupsForList(root.popupList)
property var appNameList: appNameListForGroups(root.groupsByAppName)
property var popupAppNameList: appNameListForGroups(root.popupGroupsByAppName)
// Quickshell's notification IDs starts at 1 on each run, while saved notifications
// can already contain higher IDs. This is for avoiding 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];
// console.log(root.popupInhibited)
if (!root.popupInhibited) {
root.popupList = [...root.popupList, 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.popupList = root.popupList.filter((notif) => notif.id !== id);
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) {
const index = root.list.findIndex((notif) => notif.id === id);
root.popupList = root.popupList.filter((notif) => notif.id !== id);
root.timeout(id);
}
function timeoutAll() {
root.list.forEach((notif) => {
root.timeout(notif.id);
})
root.popupList = []
}
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)
}
}
}
}