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