aboutsummaryrefslogtreecommitdiff
path: root/modules/system/quickshell/NotificationCard.qml
diff options
context:
space:
mode:
Diffstat (limited to 'modules/system/quickshell/NotificationCard.qml')
-rw-r--r--modules/system/quickshell/NotificationCard.qml405
1 files changed, 405 insertions, 0 deletions
diff --git a/modules/system/quickshell/NotificationCard.qml b/modules/system/quickshell/NotificationCard.qml
new file mode 100644
index 0000000..8b6478a
--- /dev/null
+++ b/modules/system/quickshell/NotificationCard.qml
@@ -0,0 +1,405 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Effects
+import Quickshell
+
+Item {
+ id: card
+
+ required property int nId
+ required property string nSummary
+ required property string nBody
+ required property string nAppName
+ required property string nAppIcon
+ required property string nImage
+ required property var nTimestamp
+ required property int nTimeout
+ required property var nActions
+ required property bool nClickable
+ required property bool nHasInlineReply
+ required property string nReplyPlaceholder
+ required property bool nResident
+
+ property bool hovered: cardHover.containsMouse || closeHover.containsMouse || (replyInput && replyInput.activeFocus)
+
+ readonly property bool hasBottom: (nActions ? nActions.count > 0 : false) || nHasInlineReply
+
+ signal actionInvoked(int id, string identifier)
+ signal replySent(int id, string text)
+ signal dismissed(int id)
+ signal activated(int id)
+ signal hideRequested(int id)
+
+ function formatTimeAgo(timestamp) {
+ const now = new Date();
+ const diffMs = now - timestamp;
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+
+ if (diffMins < 1) return "Just now";
+ if (diffMins < 60) return diffMins + "m ago";
+ if (diffHours < 24) return diffHours + "h ago";
+ return Qt.formatTime(timestamp, "h:mm p");
+ }
+
+ width: ListView.view ? ListView.view.width : 400
+ height: contentColumn.implicitHeight + 32
+ Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.OutSine } }
+
+ Squircle {
+ anchors.fill: parent
+ cornerRadius: 16
+ fillColor: Theme.surface
+ strokeColor: Theme.border
+ strokeWidth: 1
+ }
+
+ MouseArea {
+ id: cardHover
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: nClickable ? Qt.PointingHandCursor : Qt.ArrowCursor
+ onClicked: {
+ if (nClickable) card.activated(nId)
+ }
+ }
+
+ ColumnLayout {
+ id: contentColumn
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ margins: 16
+ }
+ spacing: 12
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 12
+ Layout.alignment: Qt.AlignTop
+
+ // LEFT: Icon
+ Item {
+ width: 44
+ height: 44
+ Layout.alignment: Qt.AlignVCenter
+
+ Squircle {
+ anchors.fill: parent
+ cornerRadius: 10
+ fillColor: Theme.surfaceLighter
+ }
+
+ Image {
+ visible: nAppIcon !== ""
+ anchors.centerIn: parent
+ width: 30; height: 30
+ source: nAppIcon
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+ }
+
+ // MIDDLE: Text
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignTop
+ spacing: 2
+
+ RowLayout {
+ Layout.fillWidth: true
+ Text {
+ text: nAppName
+ color: Theme.text
+ font {
+ family: Theme.mainFont
+ pixelSize: 12
+ weight: Font.Medium
+ }
+ opacity: 0.8
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ }
+ Text {
+ text: card.formatTimeAgo(nTimestamp)
+ color: Theme.text
+ opacity: 0.5
+ font {
+ family: Theme.mainFont
+ pixelSize: 11
+ }
+
+ Timer {
+ interval: 60000
+ running: true
+ repeat: true
+ onTriggered: parent.text = card.formatTimeAgo(nTimestamp)
+ }
+ }
+ }
+
+ Text {
+ visible: nSummary !== ""
+ Layout.fillWidth: true
+ text: nSummary
+ color: Theme.text
+ font {
+ family: Theme.mainFont
+ pixelSize: 14
+ weight: Font.DemiBold
+ }
+ wrapMode: Text.WordWrap
+ maximumLineCount: 2
+ elide: Text.ElideRight
+ }
+
+ Text {
+ visible: nBody !== ""
+ Layout.fillWidth: true
+ text: nBody
+ color: Theme.text
+ opacity: 0.85
+ font {
+ family: Theme.mainFont
+ pixelSize: 13
+ }
+ wrapMode: Text.WordWrap
+ maximumLineCount: 3
+ elide: Text.ElideRight
+ textFormat: Text.StyledText
+ }
+ }
+
+ // RIGHT: Image
+ Item {
+ visible: nImage !== ""
+ width: visible ? 44 : 0
+ height: 44
+ Layout.alignment: Qt.AlignTop
+
+ Squircle {
+ id: mediaMask
+ anchors.fill: parent
+ cornerRadius: 8
+ fillColor: Theme.text
+ visible: false
+ layer.enabled: true
+ layer.smooth: true
+ }
+
+ Image {
+ anchors.fill: parent
+ source: nImage
+ fillMode: Image.PreserveAspectCrop
+ smooth: true
+ asynchronous: true
+ layer.enabled: true
+ layer.effect: MultiEffect {
+ maskEnabled: true
+ maskSource: mediaMask
+ maskThresholdMin: 0.5
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: bottomStack
+ Layout.fillWidth: true
+ visible: card.hasBottom
+ spacing: 8
+ Layout.leftMargin: 56 // Align with text
+
+ RowLayout {
+ visible: nActions && nActions.count > 0
+ Layout.fillWidth: true
+ spacing: 8
+
+ Repeater {
+ model: nActions
+ delegate: Item {
+ id: actionBtn
+ required property int index
+ required property string identifier
+ required property string text
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: 28
+
+ Squircle {
+ anchors.fill: parent
+ cornerRadius: 6
+ fillColor: actionHover.containsMouse ? Theme.surfaceHover : Theme.surfaceLighter
+ scale: actionHover.pressed ? 0.95 : 1.0
+ Behavior on scale { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
+ }
+
+ Text {
+ anchors.centerIn: parent
+ text: actionBtn.text
+ color: Theme.text
+ font {
+ family: Theme.mainFont
+ pixelSize: 12
+ weight: Font.Medium
+ }
+ }
+
+ MouseArea {
+ id: actionHover
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: card.actionInvoked(card.nId, actionBtn.identifier)
+ }
+ }
+ }
+ }
+
+ Item {
+ visible: nHasInlineReply
+ Layout.fillWidth: true
+ Layout.preferredHeight: 30
+
+ Squircle {
+ anchors.fill: parent
+ cornerRadius: 6
+ fillColor: Theme.surfaceLighter
+ }
+
+ TextInput {
+ id: replyInput
+ anchors {
+ left: parent.left; leftMargin: 10
+ right: sendBtn.left; rightMargin: 6
+ top: parent.top; bottom: parent.bottom
+ }
+ verticalAlignment: TextInput.AlignVCenter
+ color: Theme.text
+ font {
+ family: Theme.mainFont
+ pixelSize: 12
+ }
+ clip: true
+ activeFocusOnTab: true
+
+ Text {
+ visible: replyInput.text === "" && !replyInput.activeFocus
+ anchors.fill: parent
+ verticalAlignment: Text.AlignVCenter
+ text: nReplyPlaceholder
+ color: Theme.text
+ opacity: 0.5
+ font: replyInput.font
+ }
+
+ Keys.onReturnPressed: if (text.length > 0) { card.replySent(card.nId, text); text = "" }
+ Keys.onEnterPressed: if (text.length > 0) { card.replySent(card.nId, text); text = "" }
+ }
+
+ Item {
+ id: sendBtn
+ anchors {
+ right: parent.right; rightMargin: 4
+ verticalCenter: parent.verticalCenter
+ }
+ width: 22; height: 22
+ opacity: replyInput.text.length > 0 ? 1.0 : 0.4
+
+ Rectangle {
+ anchors.fill: parent
+ radius: 4
+ color: Theme.text
+ }
+
+ Text {
+ anchors.centerIn: parent
+ text: "↑"
+ color: Theme.bg
+ font.pixelSize: 14
+ font.weight: Font.DemiBold
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: replyInput.text.length > 0
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ card.replySent(card.nId, replyInput.text)
+ replyInput.text = ""
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Close button — top-left, visible on hover
+ Rectangle {
+ width: 20
+ height: 20
+ radius: 10
+ anchors {
+ top: parent.top
+ left: parent.left
+ topMargin: -6
+ leftMargin: -6
+ }
+ color: Theme.surfaceLighter
+ border.color: Theme.border
+ border.width: 1
+ opacity: card.hovered ? 1.0 : 0.0
+ Behavior on opacity { NumberAnimation { duration: 150 } }
+
+ Item {
+ anchors.centerIn: parent
+ width: 8
+ height: 8
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: parent.width * Math.SQRT2
+ height: 1.2
+ radius: 0.6
+ color: Theme.text
+ rotation: 45
+ antialiasing: true
+ }
+ Rectangle {
+ anchors.centerIn: parent
+ width: parent.width * Math.SQRT2
+ height: 1.2
+ radius: 0.6
+ color: Theme.text
+ rotation: -45
+ antialiasing: true
+ }
+ }
+
+ MouseArea {
+ id: closeHover
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: card.dismissed(card.nId)
+ }
+ }
+
+ Timer {
+ id: hideTimer
+ interval: nTimeout
+ running: !nResident
+ onTriggered: card.hideRequested(nId)
+ }
+
+ Connections {
+ target: card
+ function onHoveredChanged() {
+ if (!nResident) {
+ if (card.hovered) hideTimer.stop()
+ else hideTimer.restart()
+ }
+ }
+ }
+} \ No newline at end of file