pragma Singleton import Quickshell import Quickshell.Services.Notifications import QtQuick Singleton { id: root property bool panelOpen: false property int unreadCount: 0 property ListModel notifications: ListModel {} property var _refs: ({}) // maps nid to the live Notification object property ListModel toasts: ListModel {} // Runs every 100ms while toasts are active, decrementing progress (1.0 to 0.0 over 5s) // and removing any toast that has expired Timer { id: toastTick interval: 100 repeat: true running: root.toasts.count > 0 onTriggered: { let i = 0 while (i < root.toasts.count) { const p = root.toasts.get(i).progress - 0.02 if (p <= 0) { root.toasts.remove(i, 1) } else { root.toasts.setProperty(i, "progress", p) i++ } } } } NotificationServer { keepOnReload: true onNotification: notif => { const ts = Date.now() const day = root._dayLabel(ts) root.notifications.insert(0, { nid: notif.id, appName: notif.appName || "Unknown", summary: notif.summary || "", body: notif.body || "", appIcon: notif.appIcon || "", urgency: notif.urgency || 1, timestamp: ts, day: day }) const r = root._refs r[notif.id] = notif root._refs = r if (!root.panelOpen) root.unreadCount++ root.toasts.insert(0, { tid: notif.id, appName: notif.appName || "Unknown", summary: notif.summary || "", body: notif.body || "", appIcon: notif.appIcon || "", urgency: notif.urgency || 1, progress: 1.0 }) } } function dismiss(index) { if (index < 0 || index >= notifications.count) return const entry = notifications.get(index) const ref = _refs[entry.nid] if (ref) { try { ref.close() } catch (_) {} const r = _refs delete r[entry.nid] _refs = r } notifications.remove(index, 1) } function dismissAll() { for (let i = 0; i < notifications.count; i++) { const ref = _refs[notifications.get(i).nid] if (ref) { try { ref.close() } catch (_) {} } } notifications.clear() _refs = {} unreadCount = 0 } // Invokes the "default" action on a notification, falling back to the first action function invokeDefault(nid) { const ref = _refs[nid] if (!ref) return const actions = ref.actions if (!actions || actions.length === 0) return for (let i = 0; i < actions.length; i++) { if (actions[i].identifier === "default") { try { actions[i].invoke() } catch (_) {} return } } try { actions[0].invoke() } catch (_) {} } function dismissToast(tid) { for (let i = 0; i < toasts.count; i++) { if (toasts.get(i).tid === tid) { toasts.remove(i, 1) return } } } function togglePanel() { panelOpen = !panelOpen if (panelOpen) unreadCount = 0 } function formatTime(ts) { return Qt.formatDateTime(new Date(ts), "hh:mm") } function _dayLabel(ts) { const now = new Date() const date = new Date(ts) const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) const yesterday = new Date(today.getTime() - 86400000) const day = new Date(date.getFullYear(), date.getMonth(), date.getDate()) if (day.getTime() === today.getTime()) return "Today" if (day.getTime() === yesterday.getTime()) return "Yesterday" return Qt.formatDateTime(date, "dd/MM/yyyy") } }