pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Services.Notifications import ZShell.Config import qs.Components import qs.Services import qs.Helpers import qs.Daemons CustomRect { id: root readonly property list activeNotifs: notifs.filter(n => !n.closed) readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" required property Flickable container readonly property bool expanded: props.expandedNotifs.includes(modelData) readonly property string image: activeNotifs.find(n => n.image.length > 0)?.image ?? "" required property string modelData readonly property int nonAnimHeight: { const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0); const columnHeight = headerHeight + notifList.layoutHeight; return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Tokens.padding.small * 2); } readonly property int notifCount: activeNotifs.length readonly property list notifs: NotifServer.list.filter(n => n.appName === modelData) required property Props props readonly property int urgency: { if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical)) return NotificationUrgency.Critical; if (activeNotifs.find(n => n.urgency === NotificationUrgency.Normal)) return NotificationUrgency.Normal; return NotificationUrgency.Low; } required property PersistentProperties visibilities function toggleExpand(expand: bool): void { if (expand) { if (!expanded) props.expandedNotifs.push(modelData); } else if (expanded) { props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1); } } anchors.left: parent?.left anchors.right: parent?.right clip: true color: Colors.layer(Colors.palette.m3surfaceContainer, 2) implicitHeight: nonAnimHeight radius: Tokens.rounding.large Behavior on implicitHeight { Anim { } } Component.onDestruction: { if (notifCount === 0 && expanded) props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1); } RowLayout { id: content anchors.left: parent.left anchors.margins: Tokens.padding.small anchors.right: parent.right anchors.top: parent.top spacing: Tokens.spacing.medium Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop implicitHeight: Config.notifs.sizes.image implicitWidth: Config.notifs.sizes.image Component { id: imageComp Image { asynchronous: true cache: false fillMode: Image.PreserveAspectCrop height: Config.notifs.sizes.image source: Qt.resolvedUrl(root.image) sourceSize: { const size = Config.notifs.sizes.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); return Qt.size(size, size); } width: Config.notifs.sizes.image } } Component { id: appIconComp ColoredIcon { color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer implicitSize: Math.round(Config.notifs.sizes.image * 0.6) layer.enabled: root.appIcon.endsWith("symbolic") source: Quickshell.iconPath(root.appIcon) } } Component { id: materialIconComp MaterialIcon { color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency) } } CustomClippingRect { anchors.fill: parent color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHigh, 3) : Colors.palette.m3secondaryContainer radius: Tokens.rounding.full Loader { anchors.centerIn: parent anchors.verticalCenterOffset: sourceComponent === materialIconComp ? 1 : 0 asynchronous: true sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { active: root.appIcon && root.image anchors.bottom: parent.bottom anchors.right: parent.right asynchronous: true sourceComponent: CustomRect { color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.palette.m3surfaceContainerHigh : Colors.palette.m3secondaryContainer implicitHeight: Config.notifs.sizes.badge implicitWidth: Config.notifs.sizes.badge radius: Tokens.rounding.full ColoredIcon { anchors.centerIn: parent color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) layer.enabled: root.appIcon.endsWith("symbolic") source: Quickshell.iconPath(root.appIcon) } } } } Column { id: column Layout.fillWidth: true spacing: root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0 Behavior on spacing { Anim { } } RowLayout { id: header anchors.left: parent.left anchors.right: parent.right spacing: Tokens.spacing.small CustomText { Layout.fillWidth: true color: Colors.palette.m3onSurfaceVariant elide: Text.ElideRight text: root.modelData } CustomText { animate: true color: Colors.palette.m3outline text: root.activeNotifs[0]?.timeStr ?? "" } CustomRect { color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : Colors.layer(Colors.palette.m3surfaceContainerHigh, 3) implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall implicitWidth: expandBtn.implicitWidth + Tokens.padding.large radius: Tokens.rounding.full StateLayer { color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurface onClicked: root.toggleExpand(!root.expanded) } RowLayout { id: expandBtn anchors.centerIn: parent spacing: Tokens.spacing.extraSmall CustomText { id: groupCount Layout.leftMargin: Tokens.padding.extraSmall / 2 animate: true color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurfaceVariant text: root.notifCount } MaterialIcon { Layout.rightMargin: -Tokens.padding.extraSmall / 2 Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.extraSmall) : 0 color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurfaceVariant rotation: root.expanded ? 180 : 0 text: "expand_more" Behavior on Layout.topMargin { Anim { } } Behavior on rotation { Anim { } } } } } } NotifGroupList { id: notifList container: root.container expanded: root.expanded notifs: root.notifs props: root.props visibilities: root.visibilities onRequestToggleExpand: expand => root.toggleExpand(expand) } } } }