import QtQuick import Quickshell import QtQuick.Controls import Quickshell.Wayland import Quickshell.Hyprland import Quickshell.Services.Notifications import "../../config" import "./notificationServer" PanelWindow { id: root implicitWidth: screen.width/5 implicitHeight: screen.height - Config.barHeight(screen.height) color: "transparent" visible: true focusable: true anchors { top: true right: true } property string textColor: Colors.primary property string textBGColor: Qt.alpha(Colors.primaryContainer, 0.75) property string bGColor: Qt.alpha(Colors.primaryContainer, 0.5) property real bigFontSize: Config.screen_height_to_font_medium(screen.height) property real fontSize: Config.screen_height_to_font_small(screen.height) property var expandedGroups: ({}) property var grouped: { const vals = NotificationS.trackedNotifications.values const len = vals.length if (len < 0) return [] let map = {} for (let i = 0; i < vals.length; ++i) { const n = vals[i] const key = (n && n.appName && n.appName !== "") ? n.appName : "Unknown" if (!map[key]) map[key] = [] map[key].push(n) } let result = [] for (const k in map) result.push({ appName: k, notes: map[k] }) result.sort((a,b)=>a.appName.localeCompare(b.appName)) return result } function toggleGroup(appName) { let copy = Object.assign({}, expandedGroups) copy[appName] = !(copy[appName] === true) expandedGroups = copy } function clearGroup(appName) { const vals = NotificationS.trackedNotifications.values.slice() for (let i=0;i1 notification are grouped) ── // Uses root.grouped (sorted by appName). Each app group with // more than one notification is rendered as a collapsible // card with a header (appName + count + expand/collapse + // clear-group). Singletons are rendered directly as a // NotificationBody without a header. Repeater { id: groupedRepeater model: root.grouped delegate: Item { id: delegateRoot required property var modelData required property int index property var group: modelData property bool isGrouped: group.notes.length > 1 property bool isExpanded: root.expandedGroups[group.appName] === true width: contentColumn.width // height adapts to which component is visible implicitHeight: isGrouped ? groupedCard.implicitHeight : singleBody.implicitHeight height: implicitHeight // ── Singleton: plain NotificationBody ── NotificationBody { id: singleBody visible: !delegateRoot.isGrouped width: parent.width screen: root.screen notification: delegateRoot.group.notes[0] isHistory: true onDismissed: function(notification, dismiss) { if (notification && notification.tracked) notification.dismiss() else if (notification && notification.close) notification.close() } } // ── Grouped card (count > 1) ── Rectangle { id: groupedCard visible: delegateRoot.isGrouped width: parent.width implicitHeight: groupColumn.implicitHeight + Config.spacing * 2 color: Qt.alpha(Colors.surfaceContainerHigh, 0.45) radius: Config.screen_big_radius_fun(root.screen.height) border.width: Config.border_width border.color: root.textColor clip: true Column { id: groupColumn anchors.fill: parent anchors.margins: Config.spacing spacing: Config.spacing Rectangle { id: groupHeader width: parent.width height: Math.max(appNameText.implicitHeight, countText.implicitHeight, expandIcon.implicitHeight, clearGroupIcon.implicitHeight) + Config.spacing * 1.5 color: root.bGColor radius: Config.radius border.width: Config.border_width border.color: root.textColor MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: root.toggleGroup(group.appName) } Row { id: headerLeft anchors.left: parent.left anchors.leftMargin: Config.spacing anchors.verticalCenter: parent.verticalCenter spacing: Config.spacing Text { id: expandIcon text: delegateRoot.isExpanded ? "󰅀" : "󰅂" font.family: Config.font font.pixelSize: root.fontSize color: root.textColor anchors.verticalCenter: parent.verticalCenter } Text { id: appNameText text: group.appName font.family: Config.font font.pixelSize: root.fontSize font.bold: true color: root.textColor elide: Text.ElideRight maximumLineCount: 1 } Text { id: countText text: "(" + group.notes.length + ")" font.family: Config.font font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) color: Qt.alpha(root.textColor, 0.8) anchors.verticalCenter: parent.verticalCenter } } Row { id: headerRight anchors.right: parent.right anchors.rightMargin: Config.spacing anchors.verticalCenter: parent.verticalCenter spacing: Config.spacing MouseArea { id: clearGroupArea width: bigFontSize + Config.spacing height: bigFontSize + Config.spacing hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: function(mouse) { mouse.accepted = true root.clearGroup(group.appName) } Rectangle{ anchors.fill: parent radius: Config.screen_big_radius_fun(screen.height) color: clearGroupArea.containsMouse ? "red" : textBGColor border.width: Config.border_width border.color: clearGroupArea.containsMouse ? "white" : textColor Text{ id: clearGroupIcon anchors.centerIn: parent text: "" font.family: Config.font font.pixelSize: bigFontSize color: clearGroupArea.containsMouse ? "white" : textColor } } } } } Column { id: notificationsColumn width: parent.width spacing: Config.spacing visible: delegateRoot.isExpanded Repeater { model: group.notes delegate: NotificationBody { required property var modelData required property int index width: notificationsColumn.width screen: root.screen notification: modelData isHistory: true onDismissed: function(notification, dismiss) { if (notification && notification.tracked) notification.dismiss() else if (notification && notification.close) notification.close() } } } } Text { visible: !delegateRoot.isExpanded && group.notes.length > 1 width: parent.width text: group.notes.length + " notification" + (group.notes.length > 1 ? "s hidden" : " hidden") + " — click header to expand" font.family: Config.font font.pixelSize: Config.screen_height_to_font_tiny(root.screen.height) color: Qt.alpha(root.textColor, 0.6) elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter } } } } } } } } } } }