diff options
| author | Leander Scherer <leander@schererleander.de> | 2026-05-18 21:48:24 +0200 |
|---|---|---|
| committer | Leander Scherer <leander@schererleander.de> | 2026-05-28 22:42:07 +0200 |
| commit | 9a7cf1242d296dbdb9c03df48ab09054960295aa (patch) | |
| tree | f1a2d5c77ef6bdb049c995afcc4c663c1ffd1373 /modules/system/quickshell/NotificationCard.qml | |
| parent | 3ef8b4973bcae26445f99467d50ad75730d204b5 (diff) | |
feat(quickshell): basic bar, tray, notification
Diffstat (limited to 'modules/system/quickshell/NotificationCard.qml')
| -rw-r--r-- | modules/system/quickshell/NotificationCard.qml | 405 |
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 |
