pragma Singleton import QtQuick import Quickshell import Quickshell.Services.Notifications import "." as M QtObject { id: root property var list: [] property bool dnd: false readonly property var popups: list.filter(n => n.popup && n.state !== "dismissed") readonly property int count: list.filter(n => n.state !== "dismissed").length // O(1) lookup property var _byId: ({}) function dismiss(notifId) { const item = _byId[notifId]; if (!item) return; item.finishDismiss(); list = list.filter(n => n !== item); delete _byId[notifId]; item.destroy(); } function dismissAll() { for (const item of list.slice()) { item.finishDismiss(); delete _byId[item.id]; item.destroy(); } list = []; } function dismissPopup(notifId) { const item = _byId[notifId]; if (item) { item.popup = false; _changed(); } } function toggleDnd() { dnd = !dnd; } function _changed() { list = list.slice(); } // Signal popups to animate out before removal signal popupExpiring(var notifId) property NotificationServer _server: NotificationServer { actionsSupported: true bodyMarkupSupported: true imageSupported: true persistenceSupported: true keepOnReload: false onNotification: notif => { notif.tracked = true; const isCritical = notif.urgency === NotificationUrgency.Critical; const item = _itemComp.createObject(root, { notification: notif, id: notif.id, summary: notif.summary, body: notif.body, appName: notif.appName, appIcon: notif.appIcon, image: notif.image, hints: notif.hints, urgency: notif.urgency, actions: notif.actions ? notif.actions.map(a => ({ identifier: a.identifier, text: a.text, invoke: () => a.invoke() })) : [], time: Date.now(), popup: isCritical || !root.dnd }); root._byId[item.id] = item; root.list = [item, ...root.list].sort((a, b) => { const aU = a.urgency === NotificationUrgency.Critical ? 1 : 0; const bU = b.urgency === NotificationUrgency.Critical ? 1 : 0; if (aU !== bU) return bU - aU; return b.time - a.time; }); // Trim excess popups const max = M.Modules.notifications.maxPopups || 4; const currentPopups = root.list.filter(n => n.popup); if (currentPopups.length > max) { for (let i = max; i < currentPopups.length; i++) currentPopups[i].popup = false; root._changed(); } // Auto-expire popup (skip for critical) if (item.popup && !isCritical) { const timeout = notif.expireTimeout > 0 ? notif.expireTimeout : (M.Modules.notifications.timeout || 3000); item._expireTimer.interval = timeout; item._expireTimer.running = true; } // Trim history (-1 = unlimited) const maxHistory = M.Modules.notifications.maxHistory ?? -1; while (maxHistory > 0 && root.list.length > maxHistory) { const old = root.list.pop(); old.finishDismiss(); delete root._byId[old.id]; old.destroy(); } } } property Component _itemComp: Component { NotifItem {} } // Single global tick for all NotifItem.timeStr bindings — replaces per-item 5s timers property real _now: Date.now() property Timer _nowTimer: Timer { running: root.count > 0 repeat: true interval: 5000 onTriggered: root._now = Date.now() } }