pragma Singleton pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Services.Notifications import qs.Common import qs.Services Singleton { id: root readonly property var notificationServer: notificationServer readonly property var notificationsNumber: notificationServer.trackedNotifications.values.length readonly property int maxShown: 3 property bool receivingLock: false property bool manualNotificationsMuted: false property bool notificationsMuted: HyprlandService.hasFullscreen || HyprlandService.isScreencasting || root.manualNotificationsMuted property ListModel trackedNotifications: ListModel {} NotificationServer { id: notificationServer imageSupported: true } Connections { target: notificationServer function onNotification(notif) { if (notif.transient || notif.body === "MediaOngoingActivity") return; notif.tracked = true; root.addNotification(root.trackedNotifications, notif); if (notif.lastGeneration) return; // Use the refactored helper if (notificationList.count < root.maxShown && !root.receivingLock) { root.addNotification(notificationList, notif); } else { root.receivingLock = true; root.addNotification(pendingList, notif); } } } ListModel { id: notificationList } ListModel { id: pendingList } /** * @param {ListModel} model * @param {var} targetNotif */ function removeNotification(model, targetNotif) { if (!model || typeof model.remove !== "function") { console.warn("removeNotification(): invalid model"); return; } for (let i = 0; i < model.count; i++) { if (model.get(i).notif === targetNotif) { model.remove(i); break; } } } /** * @param {ListModel} model * @param {var} notif */ function addNotification(model, notif) { if (!model || typeof model.append !== "function") { console.warn("addNotification(): invalid model"); return; } // Avoid duplicates for (let i = 0; i < model.count; i++) { if (model.get(i).notif === notif) return; } model.append({ notif }); notif.closed.connect(function (reason) { removeNotification(model, notif); }); } function notificationDismiss(notif) { let pendingIdx = -1; for (let i = 0; i < pendingList.count; i++) { if (pendingList.get(i).notif === notif) { pendingIdx = i; break; } } if (pendingIdx >= 0) { pendingList.remove(pendingIdx, 1); } else { removeNotification(notificationList, notif); } removeNotification(trackedNotifications, notif); if (notif && typeof notif.dismiss === "function") notif.dismiss(); tryShowNext(); } function timeoutNotification(notif) { removeNotification(notificationList, notif); tryShowNext(); } function tryShowNext() { let filled = false; while (notificationList.count < root.maxShown && pendingList.count > 0) { const nextNotif = pendingList.get(0).notif; pendingList.remove(0, 1); addNotification(notificationList, nextNotif); filled = true; } // Only lock if there are still more pending than fit onscreen root.receivingLock = pendingList.count > 0; } LazyLoader { id: popupLoader active: notificationList.count && !root.notificationsMuted component: PanelWindow { id: notificationWindow screen: { Quickshell.screens.filter(screen => screen.name == HyprlandService.focusedMon.name)[0]; } anchors.top: true exclusiveZone: 0 implicitWidth: 400 implicitHeight: screen.height color: "transparent" mask: Region { item: listView } ListView { id: listView model: notificationList implicitWidth: parent.width implicitHeight: contentHeight orientation: ListView.Vertical clip: true spacing: 5 interactive: false delegate: NotificationWrapper { id: notifWrapper required property var modelData notification: modelData implicitWidth: listView.width startTimer: NotificationUrgency.toString(notification?.urgency) === "Critical" ? false : true timerDuration: 5000 onDismissed: { if (notification && typeof notification.dismiss === "function") root.notificationDismiss(notification); } onTimedout: { root.timeoutNotification(notification); } } Component.onCompleted: positionViewAtEnd() } } } }