import QtQuick import Quickshell import Quickshell.Services.Notifications import "." as M // Shared notification card: background, progress bar, urgency bar, icon, text, dismiss button. // Does NOT include dismiss animation or dismiss logic — emits dismissRequested() instead. Item { id: root required property var notif // NotifItem (may be null — all accesses use ?.) property bool showAppName: true property bool dismissOnAction: true property int iconSize: 32 property int bodyMaxLines: 3 property color accentColor: M.Theme.base0D signal dismissRequested // Tall enough for content including padding, or the icon if it's taller implicitHeight: Math.max(_contentCol.implicitHeight, _icon.visible ? _icon.height : 0) + 16 HoverHandler { id: _hover } // Background: base01, base02 on hover Rectangle { anchors.fill: parent color: _hover.hovered ? M.Theme.base02 : M.Theme.base01 opacity: _hover.hovered ? 1.0 : Math.max(M.Theme.barOpacity, 0.9) radius: M.Theme.radius Behavior on color { ColorAnimation { duration: 100 } } // Progress bar fill (hint value) Rectangle { visible: (root.notif?.hints?.value ?? -1) >= 0 anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left width: parent.width * Math.min(1, Math.max(0, (root.notif?.hints?.value ?? 0) / 100)) color: M.Theme.base03 radius: parent.radius Behavior on width { NumberAnimation { duration: 200 } } } } // Urgency accent bar Rectangle { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom width: 3 radius: 1 color: { const u = root.notif?.urgency ?? NotificationUrgency.Normal; return u === NotificationUrgency.Critical ? M.Theme.base08 : u === NotificationUrgency.Low ? M.Theme.base04 : M.Theme.base0D; } } // App icon / image Image { id: _icon anchors.left: parent.left anchors.leftMargin: 14 anchors.top: parent.top anchors.topMargin: 8 width: root.iconSize height: root.iconSize source: { const img = root.notif?.image; if (img) return img; const ic = root.notif?.appIcon; if (!ic) return ""; return (ic.startsWith("/") || ic.startsWith("file://")) ? ic : Quickshell.iconPath(ic, "dialog-information"); } visible: status === Image.Ready fillMode: Image.PreserveAspectFit sourceSize: Qt.size(root.iconSize, root.iconSize) asynchronous: true } // Dismiss button — overlays top-right, visible only on hover (opacity keeps layout stable) Text { id: _dismissBtn anchors.right: parent.right anchors.rightMargin: 10 anchors.top: parent.top anchors.topMargin: 8 text: "\uF00D" color: _dismissHover.hovered ? M.Theme.base08 : M.Theme.base03 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.iconFontFamily opacity: _hover.hovered ? 1 : 0 HoverHandler { id: _dismissHover } TapHandler { cursorShape: Qt.PointingHandCursor onTapped: root.dismissRequested() } } Column { id: _contentCol anchors.left: _icon.visible ? _icon.right : parent.left anchors.right: parent.right anchors.top: parent.top anchors.leftMargin: _icon.visible ? 8 : 14 anchors.rightMargin: 20 anchors.topMargin: 8 spacing: 2 // Text section — tappable for default action Item { id: _textSection width: parent.width height: _textCol.implicitHeight implicitHeight: _textCol.implicitHeight TapHandler { cursorShape: root.notif?.actions?.some(a => a.identifier === "default") ? Qt.PointingHandCursor : undefined onTapped: { const def = root.notif?.actions?.find(a => a.identifier === "default"); if (def) { def.invoke(); if (root.dismissOnAction) root.dismissRequested(); } } } Column { id: _textCol width: parent.width spacing: 2 // App name + time row (optional) Row { visible: root.showAppName width: parent.width Text { text: root.notif?.appName ?? "Notification" color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily width: parent.width - _timeText.implicitWidth - 4 elide: Text.ElideRight } Text { id: _timeText text: root.notif?.timeStr ?? "" color: M.Theme.base03 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily } } // Summary (with inline time when app name row is hidden) Row { visible: !root.showAppName width: parent.width Text { text: root.notif?.summary ?? "" color: M.Theme.base05 font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily font.bold: true elide: Text.ElideRight width: parent.width - _inlineTime.implicitWidth - 4 } Text { id: _inlineTime text: root.notif?.timeStr ?? "" color: M.Theme.base03 font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily anchors.verticalCenter: parent.verticalCenter } } Text { visible: root.showAppName width: parent.width text: root.notif?.summary ?? "" color: M.Theme.base05 font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily font.bold: true elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: 2 } Text { width: parent.width text: root.notif?.body ?? "" color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.fontFamily elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: root.bodyMaxLines visible: text !== "" } } } // Action buttons — filter "default" (click-notification convention) and empty labels Row { spacing: 6 visible: _actionRepeater.count > 0 Repeater { id: _actionRepeater model: (root.notif?.actions ?? []).filter(a => a.text && a.identifier !== "default") delegate: Rectangle { required property var modelData width: _actText.implicitWidth + 12 height: _actText.implicitHeight + 6 radius: M.Theme.radius color: _actHover.hovered ? M.Theme.base03 : "transparent" border.color: M.Theme.base03 border.width: 1 Text { id: _actText anchors.centerIn: parent text: parent.modelData.text color: root.accentColor font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily } HoverHandler { id: _actHover } TapHandler { cursorShape: Qt.PointingHandCursor onTapped: { parent.modelData.invoke(); if (root.dismissOnAction) root.dismissRequested(); } } } } } } }