pragma Singleton pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Services.Notifications import qs.Widgets import qs.Services Singleton { id: root readonly property var notificationServer: notificationServer readonly property var notificationsNumber: notificationServer.trackedNotifications.values.length readonly property var 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(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 margins.top: screen.height / 100 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() } } } }