pragma Singleton import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Notifications QtObject { id: root property var list: [] property bool dnd: false readonly property var active: list.filter(n => !n.closed) readonly property var popups: list.filter(n => n.popup && !n.closed) readonly property int count: active.length function dismiss(notifId) { const n = list.find(n => n.id === notifId); if (n) { n.popup = false; n.closed = true; n.notification?.dismiss(); _changed(); } } function dismissAll() { for (const n of list.slice()) { n.popup = false; n.closed = true; n.notification?.dismiss(); } _changed(); } function dismissPopup(notifId) { const n = list.find(n => n.id === notifId); if (n) { n.popup = false; _changed(); } } function toggleDnd() { dnd = !dnd; } function _changed() { list = list.slice(); _saveTimer.restart(); } property NotificationServer _server: NotificationServer { actionsSupported: true bodyMarkupSupported: true imageSupported: true persistenceSupported: true keepOnReload: false onNotification: notif => { notif.tracked = true; const data = { id: notif.id, summary: notif.summary, body: notif.body, appName: notif.appName, appIcon: notif.appIcon, image: notif.image, urgency: notif.urgency, actions: notif.actions ? notif.actions.map(a => ({ identifier: a.identifier, text: a.text, invoke: () => a.invoke() })) : [], time: Date.now(), popup: !root.dnd, closed: false, notification: notif }; root.list = [data, ...root.list]; // Auto-expire popup if (data.popup) { const timeout = notif.expireTimeout > 0 ? notif.expireTimeout : 5000; Qt.callLater(() => { _expireTimer.createObject(root, { _notifId: data.id, interval: timeout }); }); } } } property Component _expireTimer: Component { Timer { property string _notifId running: true onTriggered: { root.dismissPopup(_notifId); destroy(); } } } // Persistence property Timer _saveTimer: Timer { interval: 1000 onTriggered: { const data = root.active.map(n => ({ id: n.id, summary: n.summary, body: n.body, appName: n.appName, appIcon: n.appIcon, image: n.image, urgency: n.urgency, time: n.time })); _storage.setText(JSON.stringify(data)); } } property FileView _storage: FileView { path: (Quickshell.env("XDG_STATE_HOME") || (Quickshell.env("HOME") + "/.local/state")) + "/nova-shell/notifs.json" onLoaded: { try { const data = JSON.parse(text()); for (const n of data) { n.popup = false; n.closed = false; n.notification = null; n.actions = []; } root.list = data.concat(root.list); } catch (e) {} } } }