diff --git a/Modules/Notifications/NotificationCard.qml b/Modules/Notifications/NotificationCard.qml index 321a8caf..33eebc07 100644 --- a/Modules/Notifications/NotificationCard.qml +++ b/Modules/Notifications/NotificationCard.qml @@ -11,48 +11,62 @@ Rectangle { property var notificationGroup property bool expanded: NotificationService.expandedGroups[notificationGroup?.key] || false + property bool descriptionExpanded: false width: parent.width height: { - if (expanded && notificationGroup && notificationGroup.count >= 1) { - const baseHeight = (116 * notificationGroup.count) + (12 * (notificationGroup.count - 1)); - const bottomMargin = notificationGroup.count === 1 ? 65 : (notificationGroup.count <= 3 ? 30 : -30); - return baseHeight + bottomMargin; + if (expanded) { + return expandedContent.height + 28; } - return 116; + const baseHeight = 116; + if (descriptionExpanded && descriptionText.hasMoreText) { + const twoLineHeight = descriptionText.font.pixelSize * 1.2 * 2; + const extraHeight = descriptionText.contentHeight - twoLineHeight; + return baseHeight + extraHeight; + } + return baseHeight; } radius: Theme.cornerRadiusLarge - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1) + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) border.color: notificationGroup?.latestNotification?.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) border.width: notificationGroup?.latestNotification?.urgency === 2 ? 2 : 1 clip: true Rectangle { - width: 4 - height: parent.height - 16 - anchors.left: parent.left - anchors.leftMargin: 2 - anchors.verticalCenter: parent.verticalCenter - radius: 2 - color: Theme.primary + anchors.fill: parent + radius: parent.radius visible: notificationGroup?.latestNotification?.urgency === 2 + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { + position: 0.0 + color: Theme.primary + } + GradientStop { + position: 0.02 + color: Theme.primary + } + GradientStop { + position: 0.021 + color: "transparent" + } + } + opacity: 1.0 } Item { id: collapsedContent - anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 12 anchors.leftMargin: 16 - anchors.rightMargin: 16 + anchors.rightMargin: 56 height: 92 visible: !expanded Rectangle { id: iconContainer - readonly property bool hasNotificationImage: notificationGroup?.latestNotification?.image && notificationGroup.latestNotification.image !== "" width: 55 @@ -70,12 +84,10 @@ Rectangle { source: { if (parent.hasNotificationImage) return notificationGroup.latestNotification.cleanImage; - if (notificationGroup?.latestNotification?.appIcon) { const appIcon = notificationGroup.latestNotification.appIcon; if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://")) return appIcon; - return Quickshell.iconPath(appIcon, ""); } return ""; @@ -121,7 +133,7 @@ Rectangle { anchors.left: iconContainer.right anchors.leftMargin: 12 - anchors.right: controlsContainer.left + anchors.right: parent.right anchors.rightMargin: 0 anchors.top: parent.top anchors.bottom: parent.bottom @@ -132,7 +144,7 @@ Rectangle { width: parent.width height: parent.height anchors.top: parent.top - anchors.topMargin: 2 + anchors.topMargin: -4 Column { width: parent.width @@ -166,95 +178,42 @@ Rectangle { } Text { - text: { - let bodyText = notificationGroup?.latestNotification?.body || ""; - const urlRegex = /(https?:\/\/[^\s]+)/g; - return bodyText.replace(urlRegex, '$1'); - } + id: descriptionText + property string fullText: notificationGroup?.latestNotification?.body || "" + property bool hasMoreText: false + + text: fullText color: Theme.surfaceVariantText font.pixelSize: Theme.fontSizeSmall width: parent.width elide: Text.ElideRight - maximumLineCount: (notificationGroup?.count || 0) > 1 ? 2 : 3 + maximumLineCount: descriptionExpanded ? -1 : 2 wrapMode: Text.WordWrap visible: text.length > 0 - textFormat: Text.RichText - onLinkActivated: function(link) { - Qt.openUrlExternally(link); + textFormat: Text.PlainText + + onContentHeightChanged: { + const singleLineHeight = font.pixelSize * 1.2; + const twoLineHeight = singleLineHeight * 2; + hasMoreText = descriptionText.contentHeight > twoLineHeight; + } + + MouseArea { + anchors.fill: parent + cursorShape: parent.hasMoreText ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: parent.hasMoreText + onClicked: { + descriptionExpanded = !descriptionExpanded; + } } } } } } - - Item { - id: controlsContainer - - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.top: parent.top - anchors.topMargin: 0 - width: (notificationGroup?.count || 0) > 1 ? 40 : 20 - height: 24 - - Rectangle { - anchors.left: parent.left - anchors.top: parent.top - width: 20 - height: 20 - radius: 10 - color: expandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - visible: (notificationGroup?.count || 0) > 1 - - DankIcon { - anchors.centerIn: parent - name: expanded ? "expand_less" : "expand_more" - size: 14 - color: Theme.surfaceText - } - - MouseArea { - id: expandArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.toggleGroupExpansion(notificationGroup?.key || "") - } - } - - Rectangle { - property bool isHovered: false - - anchors.right: parent.right - anchors.top: parent.top - width: 20 - height: 20 - radius: 10 - color: isHovered ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - - DankIcon { - name: "close" - size: 14 - color: parent.isHovered ? Theme.primary : Theme.surfaceText - anchors.centerIn: parent - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: parent.isHovered = true - onExited: parent.isHovered = false - onClicked: NotificationService.dismissGroup(notificationGroup?.key || "") - } - } - } } Column { id: expandedContent - anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right @@ -271,6 +230,8 @@ Rectangle { Row { anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 56 anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingS @@ -280,6 +241,8 @@ Rectangle { font.pixelSize: Theme.fontSizeLarge font.weight: Font.Bold anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + maximumLineCount: 1 } Rectangle { @@ -300,61 +263,6 @@ Rectangle { } } - Item { - width: 48 - height: 24 - anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: 2 - - Rectangle { - width: 20 - height: 20 - radius: 10 - anchors.left: parent.left - color: collapseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - - DankIcon { - anchors.centerIn: parent - name: "expand_less" - size: 14 - color: Theme.surfaceText - } - - MouseArea { - id: collapseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.toggleGroupExpansion(notificationGroup?.key || "") - } - } - - Rectangle { - width: 20 - height: 20 - radius: 10 - anchors.right: parent.right - color: dismissAllArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - - DankIcon { - anchors.centerIn: parent - name: "close" - size: 14 - color: Theme.surfaceText - } - - MouseArea { - id: dismissAllArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.dismissGroup(notificationGroup?.key || "") - } - } - } } Column { @@ -369,15 +277,33 @@ Rectangle { readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false width: parent.width - height: messageExpanded ? Math.min(120, 50 + (bodyText.contentHeight || 0)) : 80 + height: { + const baseHeight = 120; + if (messageExpanded) { + const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2; + if (bodyText.implicitHeight > twoLineHeight + 2) { + const extraHeight = bodyText.implicitHeight - twoLineHeight; + return baseHeight + extraHeight; + } + } + return baseHeight; + } radius: Theme.cornerRadiusLarge - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2) + color: "transparent" border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) border.width: 1 + + Behavior on height { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } Item { anchors.fill: parent anchors.margins: 12 + anchors.bottomMargin: 18 Rectangle { id: messageIcon @@ -388,7 +314,7 @@ Rectangle { height: 32 radius: 16 anchors.left: parent.left - anchors.top: parent.top + anchors.verticalCenter: parent.verticalCenter color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) border.width: 1 @@ -428,26 +354,21 @@ Rectangle { Column { anchors.left: messageIcon.right anchors.leftMargin: 12 - anchors.right: messageControls.left - anchors.rightMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 12 anchors.top: parent.top - spacing: 4 + anchors.topMargin: -2 + spacing: 2 Text { width: parent.width - text: { - const appName = modelData?.appName || ""; - const timeStr = modelData?.timeStr || ""; - if (timeStr.length > 0) - return appName + " • " + timeStr; - else - return appName; - } + text: modelData?.timeStr || "" color: Theme.surfaceVariantText font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium elide: Text.ElideRight maximumLineCount: 1 + visible: text.length > 0 } Text { @@ -463,85 +384,94 @@ Rectangle { Text { id: bodyText - - width: parent.width - text: { - let bodyText = modelData?.body || ""; - if (messageExpanded) - bodyText = bodyText.length > 500 ? bodyText.substring(0, 497) + "..." : bodyText; - else - bodyText = bodyText.length > 80 ? bodyText.substring(0, 77) + "..." : bodyText; - const urlRegex = /(https?:\/\/[^\s]+)/g; - return bodyText.replace(urlRegex, '$1'); - } + + text: modelData?.body || "" color: Theme.surfaceVariantText font.pixelSize: Theme.fontSizeSmall + width: parent.width elide: messageExpanded ? Text.ElideNone : Text.ElideRight maximumLineCount: messageExpanded ? -1 : 2 wrapMode: Text.WordWrap visible: text.length > 0 - textFormat: Text.RichText - onLinkActivated: function(link) { - Qt.openUrlExternally(link); - } - } - } - - Row { - id: messageControls - - anchors.right: parent.right - anchors.rightMargin: -6 - anchors.top: parent.top - spacing: 4 - - Rectangle { - width: 20 - height: 20 - radius: 10 - color: expandMessageArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - visible: (modelData?.body || "").length > 80 - - DankIcon { - anchors.centerIn: parent - name: { - const messageExpanded = NotificationService.expandedMessages[modelData?.notification?.id] || false; - return messageExpanded ? "expand_less" : "expand_more"; + textFormat: Text.PlainText + + MouseArea { + anchors.fill: parent + cursorShape: parent.truncated ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: parent.truncated || messageExpanded + onClicked: { + NotificationService.toggleMessageExpansion(modelData?.notification?.id || ""); } - size: 12 - color: Theme.surfaceText - } - - MouseArea { - id: expandMessageArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.toggleMessageExpansion(modelData?.notification?.id || "") } } - Rectangle { - width: 20 - height: 20 - radius: 10 - color: closeMessageArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + Row { + anchors.right: parent.right + spacing: 8 + anchors.topMargin: 4 + anchors.bottomMargin: 6 - DankIcon { - anchors.centerIn: parent - name: "close" - size: 12 - color: closeMessageArea.containsMouse ? Theme.primary : Theme.surfaceText + Repeater { + model: modelData?.actions || [] + + Rectangle { + property bool isHovered: false + + width: Math.max(actionText.implicitWidth + 12, 50) + height: 24 + radius: 4 + color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + + Text { + id: actionText + text: modelData.text || "" + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: { + if (modelData && modelData.invoke) { + modelData.invoke(); + } + } + } + } } - MouseArea { - id: closeMessageArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.dismissNotification(modelData) + Rectangle { + property bool isHovered: false + + width: Math.max(dismissText.implicitWidth + 12, 50) + height: 24 + radius: 4 + color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + + Text { + id: dismissText + text: "Dismiss" + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: NotificationService.dismissNotification(modelData) + } } } } @@ -551,49 +481,46 @@ Rectangle { } } - Rectangle { - property bool isHovered: false - + Row { + visible: !expanded anchors.right: dismissButton.left - anchors.rightMargin: 4 + anchors.rightMargin: 8 anchors.bottom: parent.bottom anchors.bottomMargin: 8 - width: viewText.width + 16 - height: viewText.height + 8 - radius: 6 - color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" - - Text { - id: viewText - - text: "View" - color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - anchors.centerIn: parent - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onEntered: parent.isHovered = true - onExited: parent.isHovered = false - onClicked: { - if (notificationGroup?.latestNotification?.actions) { - for (const action of notificationGroup.latestNotification.actions) { - if (action.text && action.text.toLowerCase() === "view") { - if (action.invoke) { - action.invoke(); - return; - } + spacing: 8 + + Repeater { + model: notificationGroup?.latestNotification?.actions || [] + + Rectangle { + property bool isHovered: false + + width: Math.max(actionText.implicitWidth + 12, 50) + height: 24 + radius: 4 + color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + + Text { + id: actionText + text: modelData.text || "" + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + elide: Text.ElideRight + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: parent.isHovered = true + onExited: parent.isHovered = false + onClicked: { + if (modelData && modelData.invoke) { + modelData.invoke(); } } - if (notificationGroup.latestNotification.actions.length > 0) { - const firstAction = notificationGroup.latestNotification.actions[0]; - if (firstAction.invoke) - firstAction.invoke(); - } } } } @@ -604,6 +531,7 @@ Rectangle { property bool isHovered: false + visible: !expanded anchors.right: parent.right anchors.rightMargin: 16 anchors.bottom: parent.bottom @@ -640,16 +568,39 @@ Rectangle { z: -1 } - Behavior on height { - SequentialAnimation { - PauseAnimation { - duration: 25 - } + Item { + id: fixedControls + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 12 + anchors.rightMargin: 16 + width: 40 + height: 24 - NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } + DankActionButton { + anchors.left: parent.left + anchors.top: parent.top + visible: (notificationGroup?.count || 0) > 1 + iconName: expanded ? "expand_less" : "expand_more" + iconSize: 14 + buttonSize: 20 + onClicked: NotificationService.toggleGroupExpansion(notificationGroup?.key || "") + } + + DankActionButton { + anchors.right: parent.right + anchors.top: parent.top + iconName: "close" + iconSize: 14 + buttonSize: 20 + onClicked: NotificationService.dismissGroup(notificationGroup?.key || "") + } + } + + Behavior on height { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing } } } \ No newline at end of file diff --git a/Modules/Notifications/NotificationCenterPopout.qml b/Modules/Notifications/NotificationCenterPopout.qml index f9aa155a..b740a3af 100644 --- a/Modules/Notifications/NotificationCenterPopout.qml +++ b/Modules/Notifications/NotificationCenterPopout.qml @@ -14,8 +14,11 @@ PanelWindow { property bool notificationHistoryVisible: false visible: notificationHistoryVisible + onNotificationHistoryVisibleChanged: { + NotificationService.disablePopups(notificationHistoryVisible); + } implicitWidth: 400 - implicitHeight: Math.min(Screen.height * 0.6, Math.max(580, 720)) + implicitHeight: Math.min(Screen.height * 0.8, 400) WlrLayershell.layer: WlrLayershell.Overlay WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: WlrKeyboardFocus.None @@ -36,8 +39,22 @@ PanelWindow { } Rectangle { + id: mainRect + + function calculateHeight() { + let baseHeight = Theme.spacingL * 2; + baseHeight += notificationHeader.height; + baseHeight += Theme.spacingM; + let listHeight = notificationList.listContentHeight; + if (NotificationService.groupedNotifications.length === 0) + listHeight = 200; + + baseHeight += Math.min(listHeight, 600); + return Math.max(300, baseHeight); + } + width: 400 - height: Math.min(Screen.height * 0.6, Math.max(580, 720)) + height: calculateHeight() x: Screen.width - width - Theme.spacingL y: Theme.barHeight + Theme.spacingXS color: Theme.popupBackground() @@ -45,103 +62,7 @@ PanelWindow { border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.width: 1 opacity: notificationHistoryVisible ? 1 : 0 - - Rectangle { - anchors.fill: parent - anchors.margins: -3 - color: "transparent" - radius: parent.radius + 3 - border.color: Qt.rgba(0, 0, 0, 0.05) - border.width: 1 - z: -3 - } - - Rectangle { - anchors.fill: parent - anchors.margins: -2 - color: "transparent" - radius: parent.radius + 2 - border.color: Qt.rgba(0, 0, 0, 0.08) - border.width: 1 - z: -2 - } - - Rectangle { - anchors.fill: parent - color: "transparent" - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) - border.width: 1 - radius: parent.radius - z: -1 - } - - transform: [ - Scale { - id: scaleTransform - - origin.x: 400 - origin.y: 0 - xScale: notificationHistoryVisible ? 1 : 0.95 - yScale: notificationHistoryVisible ? 1 : 0.8 - }, - Translate { - id: translateTransform - - x: notificationHistoryVisible ? 0 : 15 - y: notificationHistoryVisible ? 0 : -30 - } - ] - - states: [ - State { - name: "visible" - when: notificationHistoryVisible - - PropertyChanges { - target: scaleTransform - xScale: 1 - yScale: 1 - } - - PropertyChanges { - target: translateTransform - x: 0 - y: 0 - } - }, - State { - name: "hidden" - when: !notificationHistoryVisible - - PropertyChanges { - target: scaleTransform - xScale: 0.95 - yScale: 0.8 - } - - PropertyChanges { - target: translateTransform - x: 15 - y: -30 - } - } - ] - - transitions: [ - Transition { - from: "*" - to: "*" - - ParallelAnimation { - NumberAnimation { - targets: [scaleTransform, translateTransform] - properties: "xScale,yScale,x,y" - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - } - } - ] + scale: notificationHistoryVisible ? 1 : 0.9 MouseArea { anchors.fill: parent @@ -150,27 +71,71 @@ PanelWindow { } Column { + id: contentColumn + anchors.fill: parent anchors.margins: Theme.spacingL spacing: Theme.spacingM - NotificationHeader {} - - NotificationList {} + NotificationHeader { + id: notificationHeader + } + + NotificationList { + id: notificationList + + width: parent.width + height: parent.height - notificationHeader.height - contentColumn.spacing + } + + } + + Connections { + function onNotificationsChanged() { + mainRect.height = mainRect.calculateHeight(); + } + + function onGroupedNotificationsChanged() { + mainRect.height = mainRect.calculateHeight(); + } + + function onExpandedGroupsChanged() { + mainRect.height = mainRect.calculateHeight(); + } + + function onExpandedMessagesChanged() { + mainRect.height = mainRect.calculateHeight(); + } + + target: NotificationService } Behavior on height { NumberAnimation { - duration: Theme.shortDuration - easing.type: Theme.standardEasing + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing } + } Behavior on opacity { NumberAnimation { - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing + duration: Anims.durMed + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasized } + } + + Behavior on scale { + NumberAnimation { + duration: Anims.durMed + easing.type: Easing.BezierSpline + easing.bezierCurve: Anims.emphasized + } + + } + } -} \ No newline at end of file + +} diff --git a/Modules/Notifications/NotificationEmptyState.qml b/Modules/Notifications/NotificationEmptyState.qml index 5061a63d..6fa5e36e 100644 --- a/Modules/Notifications/NotificationEmptyState.qml +++ b/Modules/Notifications/NotificationEmptyState.qml @@ -5,14 +5,14 @@ import qs.Widgets Item { id: root - + width: parent.width height: 200 visible: NotificationService.notifications.length === 0 Column { anchors.centerIn: parent - spacing: Theme.spacingM + spacing: Theme.spacingXS width: parent.width * 0.8 DankIcon { @@ -24,21 +24,13 @@ Item { Text { anchors.horizontalCenter: parent.horizontalCenter - text: "No notifications" + text: "Nothing to see here" font.pixelSize: Theme.fontSizeLarge - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) font.weight: Font.Medium horizontalAlignment: Text.AlignHCenter } - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: "Notifications will appear here" - font.pixelSize: Theme.fontSizeMedium - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - width: parent.width - } } -} \ No newline at end of file + +} diff --git a/Modules/Notifications/NotificationHeader.qml b/Modules/Notifications/NotificationHeader.qml index 43be85f4..94f57763 100644 --- a/Modules/Notifications/NotificationHeader.qml +++ b/Modules/Notifications/NotificationHeader.qml @@ -6,7 +6,7 @@ import qs.Widgets Item { id: root - + width: parent.width height: 32 @@ -48,11 +48,12 @@ Item { font.weight: Font.Medium anchors.verticalCenter: parent.verticalCenter } + } MouseArea { id: clearArea - + anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor @@ -64,6 +65,7 @@ Item { duration: Theme.shortDuration easing.type: Theme.standardEasing } + } Behavior on border.color { @@ -71,6 +73,9 @@ Item { duration: Theme.shortDuration easing.type: Theme.standardEasing } + } + } -} \ No newline at end of file + +} diff --git a/Modules/Notifications/NotificationList.qml b/Modules/Notifications/NotificationList.qml index 63c5b029..86010c72 100644 --- a/Modules/Notifications/NotificationList.qml +++ b/Modules/Notifications/NotificationList.qml @@ -3,89 +3,116 @@ import QtQuick.Controls import qs.Common import qs.Services -ScrollView { +ListView { id: root - + + property alias count: root.count + readonly property real listContentHeight: root.contentHeight + readonly property bool atYBeginning: root.contentY === 0 + property real stableY: 0 + property bool isUserScrolling: false + width: parent.width - height: parent.height - 140 + height: parent.height clip: true - contentWidth: -1 - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded + model: NotificationService.groupedNotifications + spacing: Theme.spacingL + interactive: true + boundsBehavior: Flickable.StopAtBounds + flickDeceleration: 1500 + maximumFlickVelocity: 2000 + cacheBuffer: 1000 + onMovementStarted: isUserScrolling = true + onMovementEnded: { + isUserScrolling = false; + if (contentY > 40) + stableY = contentY; - ListView { - id: notificationsList + } + onContentYChanged: { + if (!isUserScrolling && visible && parent.visible && stableY > 40 && Math.abs(contentY - stableY) > 10) + contentY = stableY; - model: NotificationService.groupedNotifications - spacing: Theme.spacingL - interactive: true - boundsBehavior: Flickable.StopAtBounds - flickDeceleration: 1500 - maximumFlickVelocity: 2000 + } + + NotificationEmptyState { + visible: root.count === 0 + anchors.centerIn: parent + } + + add: Transition { + enabled: !root.isUserScrolling + + ParallelAnimation { + NumberAnimation { + properties: "opacity" + from: 0 + to: 1 + duration: root.isUserScrolling ? 0 : Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + NumberAnimation { + properties: "height" + from: 0 + duration: root.isUserScrolling ? 0 : Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + + } + + } + + remove: Transition { + SequentialAnimation { + PauseAnimation { + duration: 50 + } - add: Transition { ParallelAnimation { NumberAnimation { properties: "opacity" - from: 0 - to: 1 + to: 0 duration: Theme.mediumDuration easing.type: Theme.emphasizedEasing } NumberAnimation { - properties: "height" - from: 0 + properties: "height,anchors.topMargin,anchors.bottomMargin" + to: 0 duration: Theme.mediumDuration easing.type: Theme.emphasizedEasing } + } + } - remove: Transition { - SequentialAnimation { - PauseAnimation { - duration: 50 - } - - ParallelAnimation { - NumberAnimation { - properties: "opacity" - to: 0 - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - - NumberAnimation { - properties: "height,anchors.topMargin,anchors.bottomMargin" - to: 0 - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - } - } - } - - displaced: Transition { - NumberAnimation { - properties: "y" - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - } - - move: Transition { - NumberAnimation { - properties: "y" - duration: Theme.mediumDuration - easing.type: Theme.emphasizedEasing - } - } - - delegate: NotificationCard { - notificationGroup: modelData - } } - NotificationEmptyState {} -} \ No newline at end of file + displaced: Transition { + enabled: !root.isUserScrolling + + NumberAnimation { + properties: "y" + duration: 0 + } + + } + + move: Transition { + enabled: !root.isUserScrolling + + NumberAnimation { + properties: "y" + duration: (root.atYBeginning && !root.isUserScrolling) ? Theme.mediumDuration : 0 + easing.type: Theme.emphasizedEasing + } + + } + + delegate: NotificationCard { + notificationGroup: modelData + } + +} diff --git a/Modules/Notifications/NotificationPopup.qml b/Modules/Notifications/NotificationPopup.qml index af17e72f..e6f54dc0 100644 --- a/Modules/Notifications/NotificationPopup.qml +++ b/Modules/Notifications/NotificationPopup.qml @@ -60,7 +60,6 @@ PanelWindow { readonly property bool isPopup: modelData.latestNotification.popup readonly property int expireTimeout: modelData.latestNotification.notification.expireTimeout property string stableGroupKey: "" - // Watch for changes to latest notification (new message joins group) property var currentLatestNotification: modelData.latestNotification Component.onCompleted: { @@ -70,7 +69,6 @@ PanelWindow { height: { if (expanded && modelData.count >= 1) { const baseHeight = (116 * modelData.count) + (12 * (modelData.count - 1)); - // Add extra bottom margin for View/Dismiss buttons when there are fewer than 3 messages const bottomMargin = modelData.count === 1 ? 70 : (modelData.count < 3 ? 50 : -28); return baseHeight + bottomMargin; } @@ -81,8 +79,12 @@ PanelWindow { border.color: modelData.latestNotification.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.width: modelData.latestNotification.urgency === 2 ? 2 : 1 clip: true - - // Material 3 elevation with multiple layers + onCurrentLatestNotificationChanged: { + if (isPopup && !cardHoverArea.containsMouse) + dismissTimer.restart(); + + } + Rectangle { anchors.fill: parent anchors.margins: -3 @@ -92,7 +94,7 @@ PanelWindow { border.width: 1 z: -3 } - + Rectangle { anchors.fill: parent anchors.margins: -2 @@ -102,7 +104,7 @@ PanelWindow { border.width: 1 z: -2 } - + Rectangle { anchors.fill: parent color: "transparent" @@ -111,24 +113,35 @@ PanelWindow { radius: parent.radius z: -1 } - onCurrentLatestNotificationChanged: { - if (isPopup && !cardHoverArea.containsMouse) - dismissTimer.restart(); - - } Rectangle { - width: 4 - height: parent.height - 16 - anchors.left: parent.left - anchors.leftMargin: 2 - anchors.verticalCenter: parent.verticalCenter - radius: 2 - color: Theme.primary + anchors.fill: parent + radius: parent.radius visible: modelData.latestNotification.urgency === 2 + opacity: 1 + + gradient: Gradient { + orientation: Gradient.Horizontal + + GradientStop { + position: 0 + color: Theme.primary + } + + GradientStop { + position: 0.02 + color: Theme.primary + } + + GradientStop { + position: 0.021 + color: "transparent" + } + + } + } - // Collapsed view - show only latest notification Item { id: collapsedContent @@ -160,11 +173,9 @@ PanelWindow { anchors.fill: parent anchors.margins: 2 source: { - // Priority 1: Use notification image if available if (parent.hasNotificationImage) return modelData.latestNotification.cleanImage; - // Priority 2: Use appIcon - handle URLs directly, use iconPath for icon names if (modelData.latestNotification.appIcon) { const appIcon = modelData.latestNotification.appIcon; if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://")) @@ -272,9 +283,7 @@ PanelWindow { } text: { - // Auto-detect and make URLs clickable, with truncation for popups let bodyText = modelData.latestNotification.body; - // Truncate to 108 characters max for popup notifications if (bodyText.length > 105) bodyText = bodyText.substring(0, 102) + "..."; @@ -307,89 +316,42 @@ PanelWindow { anchors.rightMargin: 0 anchors.top: parent.top anchors.topMargin: 0 - width: modelData.count > 1 ? 40 : 20 // Dynamic width: 40px for expand+close, 20px for close only + width: modelData.count > 1 ? 40 : 20 height: 24 - // Expand button - always takes up space but only visible when needed - Rectangle { - id: collapsedExpandButton - + DankActionButton { anchors.left: parent.left anchors.top: parent.top - width: 20 - height: 20 - radius: 10 - color: expandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" visible: modelData.count > 1 - - DankIcon { - anchors.centerIn: parent - name: expanded ? "expand_less" : "expand_more" - size: 14 - color: Theme.surfaceText - } - - MouseArea { - id: expandArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.toggleGroupExpansion(modelData.key) - } - + iconName: expanded ? "expand_less" : "expand_more" + iconSize: 14 + buttonSize: 20 + z: 15 + onClicked: NotificationService.toggleGroupExpansion(modelData.key) } - // Close button - always positioned at the right edge - Rectangle { - id: closeButton - - property bool isHovered: false - + DankActionButton { anchors.right: parent.right anchors.top: parent.top - width: 20 - height: 20 - radius: 10 - color: isHovered ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - z: 10 - - DankIcon { - id: closeIcon - - name: "close" - size: 14 - color: closeButton.isHovered ? Theme.primary : Theme.surfaceText - anchors.centerIn: parent - } - - MouseArea { - id: dismissArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - z: 11 - onEntered: { - closeButton.isHovered = true; - dismissTimer.stop(); + iconName: "close" + iconSize: 14 + buttonSize: 20 + z: 15 + onClicked: { + if (modelData.latestNotification.notification.transient) { + NotificationService.dismissGroup(modelData.key); + } else { + for (const notif of modelData.notifications) { + notif.popup = false; + } } - onExited: { - closeButton.isHovered = false; - if (modelData.latestNotification.popup && !cardHoverArea.containsMouse) - dismissTimer.restart(); - - } - onClicked: NotificationService.dismissGroup(modelData.key) } - } } } - // Expanded view - show all notifications in group Item { anchors.fill: parent anchors.margins: 16 @@ -401,7 +363,6 @@ PanelWindow { width: parent.width spacing: 10 - // Header with app name and count Item { width: parent.width height: 32 @@ -449,52 +410,28 @@ PanelWindow { anchors.fill: parent spacing: 8 - Rectangle { - width: 20 - height: 20 - radius: 10 - color: expandedExpandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - - DankIcon { - anchors.centerIn: parent - name: "expand_less" - size: 14 - color: Theme.surfaceText - } - - MouseArea { - id: expandedExpandArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.toggleGroupExpansion(modelData.key) - } - + DankActionButton { + iconName: "expand_less" + iconSize: 14 + buttonSize: 20 + z: 15 + onClicked: NotificationService.toggleGroupExpansion(modelData.key) } - Rectangle { - width: 20 - height: 20 - radius: 10 - color: expandedCloseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - - DankIcon { - anchors.centerIn: parent - name: "close" - size: 14 - color: expandedCloseArea.containsMouse ? Theme.primary : Theme.surfaceText + DankActionButton { + iconName: "close" + iconSize: 14 + buttonSize: 20 + z: 15 + onClicked: { + if (modelData.latestNotification.notification.transient) { + NotificationService.dismissGroup(modelData.key); + } else { + for (const notif of modelData.notifications) { + notif.popup = false; + } + } } - - MouseArea { - id: expandedCloseArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.dismissGroup(modelData.key) - } - } } @@ -503,7 +440,6 @@ PanelWindow { } - // Scrollable list of individual notifications Rectangle { width: parent.width height: Math.min(400, modelData.notifications.length * 90) // Fixed height constraint for inner scroll @@ -669,56 +605,22 @@ PanelWindow { spacing: 4 // Expand/collapse button for individual message - Rectangle { - id: expandButton - - width: 20 - height: 20 - radius: 10 - color: expandMessageArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" + DankActionButton { visible: (modelData.body || "").length > 80 - - DankIcon { - anchors.centerIn: parent - name: messageExpanded ? "expand_less" : "expand_more" - size: 12 - color: Theme.surfaceText - } - - MouseArea { - id: expandMessageArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.toggleMessageExpansion(modelData.notification.id) - } - + iconName: messageExpanded ? "expand_less" : "expand_more" + iconSize: 12 + buttonSize: 20 + z: 15 + onClicked: NotificationService.toggleMessageExpansion(modelData.notification.id) } // Close button for individual message - Rectangle { - width: 20 - height: 20 - radius: 10 - color: closeMessageArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" - - DankIcon { - anchors.centerIn: parent - name: "close" - size: 12 - color: closeMessageArea.containsMouse ? Theme.primary : Theme.surfaceText - } - - MouseArea { - id: closeMessageArea - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: NotificationService.dismissNotification(modelData) - } - + DankActionButton { + iconName: "close" + iconSize: 12 + buttonSize: 20 + z: 15 + onClicked: NotificationService.dismissNotification(modelData) } } @@ -747,13 +649,13 @@ PanelWindow { } - // Main hover area for persistence + // Main hover area for persistence and click handling MouseArea { id: cardHoverArea anchors.fill: parent hoverEnabled: true - acceptedButtons: Qt.NoButton + acceptedButtons: Qt.LeftButton propagateComposedEvents: true z: 0 onEntered: { @@ -764,71 +666,72 @@ PanelWindow { dismissTimer.restart(); } + onClicked: { + if (modelData.latestNotification.notification.transient) { + NotificationService.dismissGroup(modelData.key); + } else { + for (const notif of modelData.notifications) { + notif.popup = false; + } + } + } } - // View button positioned at bottom-right of notification card - Rectangle { - id: viewButton - - property bool isHovered: false - + // Action buttons positioned at bottom-left of notification card + Row { anchors.right: dismissButton.left - anchors.rightMargin: 4 + anchors.rightMargin: 8 anchors.bottom: parent.bottom anchors.bottomMargin: 8 - width: viewText.width + 16 - height: viewText.height + 8 - radius: 6 - color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + spacing: 4 z: 10 - Text { - id: viewText + Repeater { + model: modelData.latestNotification.actions || [] - text: "View" - color: viewButton.isHovered ? Theme.primary : Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - anchors.centerIn: parent - } + Rectangle { + property bool isHovered: false - MouseArea { - id: viewArea + width: Math.min(actionText.contentWidth + 12, 70) + height: 24 + radius: 4 + color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - z: 11 - onEntered: { - viewButton.isHovered = true; - dismissTimer.stop(); - } - onExited: { - viewButton.isHovered = false; - if (modelData.latestNotification.popup && !cardHoverArea.containsMouse) - dismissTimer.restart(); + Text { + id: actionText - } - onClicked: { - // Handle navigation to source message - if (modelData.latestNotification.actions) { - for (const action of modelData.latestNotification.actions) { - if (action.text && action.text.toLowerCase() === "view") { - if (action.invoke) { - action.invoke(); - return ; - } - } + text: modelData.text || "" + color: parent.isHovered ? Theme.primary : Theme.surfaceVariantText + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + anchors.centerIn: parent + elide: Text.ElideRight + width: Math.min(contentWidth, parent.width - 8) + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: { + parent.isHovered = true; + dismissTimer.stop(); } - // If no View action, try the first available action - if (modelData.latestNotification.actions.length > 0) { - const firstAction = modelData.latestNotification.actions[0]; - if (firstAction.invoke) - firstAction.invoke(); + onExited: { + parent.isHovered = false; + if (modelData.latestNotification.popup && !cardHoverArea.containsMouse) + dismissTimer.restart(); + + } + onClicked: { + if (modelData && modelData.invoke) + modelData.invoke(); } } + } + } } @@ -877,12 +780,7 @@ PanelWindow { } onClicked: { - // Move to notification center (don't close) - const groupKey = stableGroupKey || modelData.key; - console.log("Manually hiding notification group from popup:", groupKey); - modelData.latestNotification.popup = false; - // Clear expansion state when manually hiding from popup - NotificationService.clearGroupExpansionState(groupKey); + NotificationService.dismissGroup(modelData.key); } } @@ -896,7 +794,6 @@ PanelWindow { onTriggered: { // Move to notification center (don't dismiss completely) const groupKey = stableGroupKey || modelData.key; - console.log("Auto-hiding notification group from popup:", groupKey); modelData.latestNotification.popup = false; // Clear expansion state when hiding from popup NotificationService.clearGroupExpansionState(groupKey); diff --git a/Modules/TopBar/TopBar.qml b/Modules/TopBar/TopBar.qml index 8fcb41e1..2b6f8fb6 100644 --- a/Modules/TopBar/TopBar.qml +++ b/Modules/TopBar/TopBar.qml @@ -323,7 +323,6 @@ PanelWindow { if (controlCenterPopout.controlCenterVisible) { if (NetworkService.wifiEnabled) NetworkService.scanWifi(); - } } } @@ -334,4 +333,4 @@ PanelWindow { } -} +} \ No newline at end of file diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml index 49f751bf..842003fd 100644 --- a/Services/NotificationService.qml +++ b/Services/NotificationService.qml @@ -16,8 +16,9 @@ Singleton { readonly property var groupedNotifications: getGroupedNotifications() readonly property var groupedPopups: getGroupedPopups() - property var expandedGroups: ({}) // Track which groups are expanded - property var expandedMessages: ({}) // Track which individual messages are expanded + property var expandedGroups: ({}) + property var expandedMessages: ({}) + property bool popupsDisabled: false // Notification persistence settings property int maxStoredNotifications: 100 @@ -29,6 +30,7 @@ Singleton { keepOnReload: false actionsSupported: true + actionIconsSupported: true bodyHyperlinksSupported: true bodyImagesSupported: true bodyMarkupSupported: true @@ -36,50 +38,23 @@ Singleton { inlineReplySupported: true onNotification: notif => { - console.log("=== RAW NOTIFICATION DATA ==="); - console.log("appName:", notif.appName); - console.log("summary:", notif.summary); - console.log("body:", notif.body); - console.log("appIcon:", notif.appIcon); - console.log("image:", notif.image); - console.log("urgency:", notif.urgency); - console.log("hasInlineReply:", notif.hasInlineReply); - console.log("============================="); - // Check if notification should be shown based on settings - if (!NotificationSettings.shouldShowNotification(notif)) { - console.log("Notification blocked by settings for app:", notif.appName); - return; - } notif.tracked = true; const wrapper = notifComponent.createObject(root, { - popup: true, + popup: !notif.transient, // Transient notifications show as popups but don't persist notification: notif }); if (wrapper) { const groupKey = getGroupKey(wrapper); - console.log("New notification added to group:", groupKey, "Expansion state:", expandedGroups[groupKey] || false); - // Handle media notification replacement - if (wrapper.isMedia) { - handleMediaNotification(wrapper); - } else { - root.notifications.push(wrapper); - } - // Don't auto-expand groups - let user control expansion state - - // Add to persistent storage (only for non-transient notifications) + // Only add to notifications list if not transient if (!notif.transient) { + root.notifications.push(wrapper); addToPersistentStorage(wrapper); } - - console.log("Notification added. Total notifications:", root.notifications.length); - console.log("Grouped notifications:", root.groupedNotifications.length); - } else { - console.error("Failed to create notification wrapper"); } } } @@ -87,7 +62,11 @@ Singleton { component NotifWrapper: QtObject { id: wrapper - property bool popup: true + property bool popup: false + + Component.onCompleted: { + popup = !root.popupsDisabled && !notification.transient; + } readonly property date time: new Date() readonly property string timeStr: { const now = new Date(); @@ -114,6 +93,7 @@ Singleton { return appIcon; } readonly property string appName: notification.appName + readonly property string desktopEntry: notification.desktopEntry readonly property string image: notification.image readonly property string cleanImage: { if (!image) return ""; @@ -128,85 +108,6 @@ Singleton { // Enhanced properties for better handling readonly property bool hasImage: image && image.length > 0 readonly property bool hasAppIcon: appIcon && appIcon.length > 0 - readonly property bool isConversation: notification.hasInlineReply - readonly property bool isMedia: isMediaNotification() - readonly property bool isSystem: isSystemNotification() - - function isMediaNotification() { - const appNameLower = appName.toLowerCase(); - const summaryLower = summary.toLowerCase(); - - // Check for media apps - if (appNameLower.includes("spotify") || - appNameLower.includes("vlc") || - appNameLower.includes("mpv") || - appNameLower.includes("music") || - appNameLower.includes("player") || - appNameLower.includes("youtube") || - appNameLower.includes("media")) { - return true; - } - - // Check for media-related summary text - if (summaryLower.includes("now playing") || - summaryLower.includes("playing") || - summaryLower.includes("paused") || - summaryLower.includes("track")) { - return true; - } - - // Check for media actions - for (const action of actions) { - const actionId = action.identifier.toLowerCase(); - if (actionId.includes("play") || - actionId.includes("pause") || - actionId.includes("next") || - actionId.includes("previous") || - actionId.includes("media")) { - return true; - } - } - - return false; - } - - function isSystemNotification() { - const appNameLower = appName.toLowerCase(); - const summaryLower = summary.toLowerCase(); - - // Check for system apps - if (appNameLower.includes("system") || - appNameLower.includes("networkmanager") || - appNameLower.includes("upower") || - appNameLower.includes("notification-daemon") || - appNameLower.includes("systemd") || - appNameLower.includes("update") || - appNameLower.includes("battery") || - appNameLower.includes("network") || - appNameLower.includes("wifi") || - appNameLower.includes("bluetooth")) { - return true; - } - - // Check for system-related summary text - if (summaryLower.includes("battery") || - summaryLower.includes("power") || - summaryLower.includes("update") || - summaryLower.includes("connected") || - summaryLower.includes("disconnected") || - summaryLower.includes("network") || - summaryLower.includes("wifi") || - summaryLower.includes("bluetooth")) { - return true; - } - - return false; - } - - - - - readonly property Connections conn: Connections { target: wrapper.notification.Retainable @@ -218,11 +119,14 @@ Singleton { const groupKey = getGroupKey(wrapper); root.notifications.splice(index, 1); - // Check if this group now has no notifications left + // Check if this group now has no notifications left or only 1 left const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey); if (remainingInGroup.length === 0) { // Immediately clear expansion state for empty group clearGroupExpansionState(groupKey); + } else if (remainingInGroup.length === 1) { + // Collapse groups that only have 1 notification left + clearGroupExpansionState(groupKey); } // Clean up all expansion states @@ -243,88 +147,43 @@ Singleton { // Helper functions function clearAllNotifications() { - // Create a copy of the array to avoid modification during iteration + // Actually dismiss all notifications from center const notificationsCopy = [...root.notifications]; - for (const notif of notificationsCopy) { + notificationsCopy.forEach(notif => { notif.notification.dismiss(); - } - // Note: Expansion states will be cleaned up by onDropped as notifications are removed + }); + // Clear all expansion states + expandedGroups = {}; + expandedMessages = {}; } function dismissNotification(wrapper) { wrapper.notification.dismiss(); } - - function handleMediaNotification(newMediaWrapper) { - const groupKey = getGroupKey(newMediaWrapper); - - // Find and replace any existing media notification from the same app - for (let i = notifications.length - 1; i >= 0; i--) { - const existing = notifications[i]; - if (existing.isMedia && getGroupKey(existing) === groupKey) { - // Replace the existing media notification - existing.notification.dismiss(); - break; + + function hidePopup(wrapper) { + wrapper.popup = false; + } + + function disablePopups(disable) { + popupsDisabled = disable; + if (disable) { + for (const notif of root.notifications) { + notif.popup = false; } } - - // Add the new media notification - root.notifications.push(newMediaWrapper); } + // Android 16-style notification grouping functions function getGroupKey(wrapper) { - const appName = wrapper.appName.toLowerCase(); - - // Media notifications: replace previous media notification from same app - if (wrapper.isMedia) { - return `${appName}:media`; + // Priority 1: Use desktopEntry if available + if (wrapper.desktopEntry && wrapper.desktopEntry !== "") { + return wrapper.desktopEntry.toLowerCase(); } - // System notifications: group by category - if (wrapper.isSystem) { - const summary = wrapper.summary.toLowerCase(); - - if (summary.includes("battery") || summary.includes("power")) { - return "system:battery"; - } - if (summary.includes("network") || summary.includes("wifi") || summary.includes("connected") || summary.includes("disconnected")) { - return "system:network"; - } - if (summary.includes("update") || summary.includes("upgrade")) { - return "system:updates"; - } - if (summary.includes("bluetooth")) { - return "system:bluetooth"; - } - - // Default system grouping - return "system:general"; - } - - // Conversation apps with inline reply - if (wrapper.isConversation) { - const summary = wrapper.summary.toLowerCase(); - - // Group by conversation/channel name from summary - if (summary.includes("#")) { - const channelMatch = summary.match(/#[\w-]+/); - if (channelMatch) { - return `${appName}:${channelMatch[0]}`; - } - } - - // Group by sender/conversation name if meaningful - if (summary && !summary.includes("new message") && !summary.includes("notification")) { - return `${appName}:${summary}`; - } - - // Default conversation grouping - return `${appName}:conversation`; - } - - // Default: Group by app - return appName; + // Priority 2: Use appName as fallback + return wrapper.appName.toLowerCase(); } function getGroupedNotifications() { @@ -340,9 +199,6 @@ Singleton { latestNotification: null, count: 0, hasInlineReply: false, - isConversation: notif.isConversation, - isMedia: notif.isMedia, - isSystem: notif.isSystem }; } @@ -356,6 +212,11 @@ Singleton { } return Object.values(groups).sort((a, b) => { + const aUrgency = a.latestNotification.urgency || 0; + const bUrgency = b.latestNotification.urgency || 0; + if (aUrgency !== bUrgency) { + return bUrgency - aUrgency; + } return b.latestNotification.time.getTime() - a.latestNotification.time.getTime(); }); } @@ -373,9 +234,6 @@ Singleton { latestNotification: null, count: 0, hasInlineReply: false, - isConversation: notif.isConversation, - isMedia: notif.isMedia, - isSystem: notif.isSystem }; } @@ -389,6 +247,11 @@ Singleton { } return Object.values(groups).sort((a, b) => { + const aUrgency = a.latestNotification.urgency || 0; + const bUrgency = b.latestNotification.urgency || 0; + if (aUrgency !== bUrgency) { + return bUrgency - aUrgency; + } return b.latestNotification.time.getTime() - a.latestNotification.time.getTime(); }); } @@ -403,18 +266,15 @@ Singleton { } function dismissGroup(groupKey) { - console.log("Completely dismissing group:", groupKey); const group = groupedNotifications.find(g => g.key === groupKey); if (group) { for (const notif of group.notifications) { notif.notification.dismiss(); } } - // Note: Expansion state will be cleaned up by onDropped when notifications are removed } function clearGroupExpansionState(groupKey) { - // Immediately remove expansion state for a specific group let newExpandedGroups = {}; for (const key in expandedGroups) { if (key !== groupKey && expandedGroups[key]) { @@ -422,22 +282,16 @@ Singleton { } } expandedGroups = newExpandedGroups; - - console.log("Cleared expansion state for group:", groupKey); } function cleanupExpansionStates() { - // Get all current group keys and message IDs const currentGroupKeys = new Set(groupedNotifications.map(g => g.key)); const currentMessageIds = new Set(); - for (const group of groupedNotifications) { for (const notif of group.notifications) { currentMessageIds.add(notif.notification.id); } } - - // Clean up expanded groups that no longer exist let newExpandedGroups = {}; for (const key in expandedGroups) { if (currentGroupKeys.has(key) && expandedGroups[key]) { @@ -445,8 +299,6 @@ Singleton { } } expandedGroups = newExpandedGroups; - - // Clean up expanded messages that no longer exist let newExpandedMessages = {}; for (const messageId in expandedMessages) { if (currentMessageIds.has(messageId) && expandedMessages[messageId]) { @@ -469,69 +321,15 @@ Singleton { if (group.count === 1) { return group.latestNotification.summary; } - - if (group.isMedia) { - return "Now Playing"; - } - - if (group.isSystem) { - const keyParts = group.key.split(":"); - if (keyParts.length > 1) { - const systemCategory = keyParts[1]; - switch (systemCategory) { - case "battery": return `${group.count} Battery alerts`; - case "network": return `${group.count} Network updates`; - case "updates": return `${group.count} System updates`; - case "bluetooth": return `${group.count} Bluetooth updates`; - default: return `${group.count} System notifications`; - } - } - return `${group.count} System notifications`; - } - - if (group.isConversation) { - const keyParts = group.key.split(":"); - if (keyParts.length > 1) { - const conversationKey = keyParts[keyParts.length - 1]; - if (conversationKey !== "conversation") { - return `${conversationKey}: ${group.count} messages`; - } - } - return `${group.count} new messages`; - } - return `${group.count} notifications`; } - function getGroupBody(group) { if (group.count === 1) { return group.latestNotification.body; } - - if (group.isMedia) { - const latest = group.latestNotification; - if (latest.body && latest.body.length > 0) { - return latest.body; - } - return latest.summary; - } - - if (group.isSystem) { - return `Latest: ${group.latestNotification.summary}`; - } - - if (group.isConversation) { - const latest = group.latestNotification; - if (latest.body && latest.body.length > 0) { - return latest.body; - } - return "Tap to view conversation"; - } - return `Latest: ${group.latestNotification.summary}`; } - // Notification persistence functions function addToPersistentStorage(wrapper) { const persistedNotif = { id: wrapper.notification.id, @@ -542,45 +340,29 @@ Singleton { image: wrapper.image, urgency: wrapper.urgency, timestamp: wrapper.time.getTime(), - isConversation: wrapper.isConversation, - isMedia: wrapper.isMedia, - isSystem: wrapper.isSystem }; - - // Add to beginning of array persistedNotifications.unshift(persistedNotif); - - // Clean up old notifications cleanupPersistentStorage(); } function cleanupPersistentStorage() { const now = new Date().getTime(); let newPersisted = []; - for (let i = 0; i < persistedNotifications.length && i < maxStoredNotifications; i++) { const notif = persistedNotifications[i]; if (now - notif.timestamp < maxNotificationAge) { newPersisted.push(notif); } } - persistedNotifications = newPersisted; } function getPersistentNotificationsByApp(appName) { return persistedNotifications.filter(notif => notif.appName.toLowerCase() === appName.toLowerCase()); } - function getPersistentNotificationsByType(type) { - switch (type) { - case "conversation": return persistedNotifications.filter(notif => notif.isConversation); - case "media": return persistedNotifications.filter(notif => notif.isMedia); - case "system": return persistedNotifications.filter(notif => notif.isSystem); - default: return persistedNotifications; - } + return persistedNotifications; } - function searchPersistentNotifications(query) { const searchLower = query.toLowerCase(); return persistedNotifications.filter(notif => @@ -589,8 +371,6 @@ Singleton { notif.body.toLowerCase().includes(searchLower) ); } - - // Initialize persistence on component creation Component.onCompleted: { cleanupPersistentStorage(); } diff --git a/Services/NotificationSettings.qml b/Services/NotificationSettings.qml deleted file mode 100644 index d08943c0..00000000 --- a/Services/NotificationSettings.qml +++ /dev/null @@ -1,194 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell - -Singleton { - id: root - - // General notification settings - property bool notificationsEnabled: true - property bool soundEnabled: true - property bool persistNotifications: true - property int defaultTimeout: 5000 // milliseconds - - // Grouping settings - property bool enableSmartGrouping: true - property bool autoExpandConversations: true - property bool replaceMediaNotifications: true - - // Persistence settings - property int maxStoredNotifications: 100 - property int notificationRetentionDays: 7 - - // Display settings - property bool showNotificationPopups: true - property bool showAppIcons: true - property bool showTimestamps: true - property bool enableInlineReply: true - property bool showActionButtons: true - - // Priority settings - property bool allowCriticalNotifications: true - property bool respectDoNotDisturb: true - - // App-specific settings - property var appSettings: ({}) - - // Do Not Disturb settings - property bool doNotDisturbMode: false - property string doNotDisturbStart: "22:00" - property string doNotDisturbEnd: "08:00" - property bool allowCriticalInDND: true - - // Sound settings - property string notificationSound: "default" - property real soundVolume: 0.7 - property bool vibrationEnabled: false - - function getAppSetting(appName, setting, defaultValue) { - const app = appSettings[appName.toLowerCase()]; - if (app && app.hasOwnProperty(setting)) { - return app[setting]; - } - return defaultValue; - } - - function setAppSetting(appName, setting, value) { - let newAppSettings = {}; - for (const app in appSettings) { - newAppSettings[app] = appSettings[app]; - } - - const appKey = appName.toLowerCase(); - if (!newAppSettings[appKey]) { - newAppSettings[appKey] = {}; - } - newAppSettings[appKey][setting] = value; - appSettings = newAppSettings; - - // Save to persistent storage - saveSettings(); - } - - function isAppBlocked(appName) { - const appKey = appName.toLowerCase(); - if (appKey === "notify-send" || appKey === "libnotify") { - return false; - } - return getAppSetting(appName, "blocked", false); - } - - function isAppMuted(appName) { - return getAppSetting(appName, "muted", false); - } - - function getAppTimeout(appName) { - return getAppSetting(appName, "timeout", defaultTimeout); - } - - function isInDoNotDisturbMode() { - if (!doNotDisturbMode && !respectDoNotDisturb) { - return false; - } - - const now = new Date(); - const currentTime = now.getHours() * 60 + now.getMinutes(); - - const startParts = doNotDisturbStart.split(":"); - const endParts = doNotDisturbEnd.split(":"); - const startTime = parseInt(startParts[0]) * 60 + parseInt(startParts[1]); - const endTime = parseInt(endParts[0]) * 60 + parseInt(endParts[1]); - - if (startTime <= endTime) { - // Same day range (e.g., 9:00 - 17:00) - return currentTime >= startTime && currentTime <= endTime; - } else { - // Overnight range (e.g., 22:00 - 08:00) - return currentTime >= startTime || currentTime <= endTime; - } - } - - function shouldShowNotification(notification) { - // Check if notifications are globally disabled - if (!notificationsEnabled) { - return false; - } - - // Check if app is blocked - if (isAppBlocked(notification.appName)) { - return false; - } - - // DND logic temporarily disabled for all notifications - // if (isInDoNotDisturbMode()) { - // // Allow critical notifications if configured - // if (allowCriticalInDND && notification.urgency === 2) { - // return true; - // } - // return false; - // } - - return true; - } - - function shouldPlaySound(notification) { - if (!soundEnabled) { - return false; - } - - if (isAppMuted(notification.appName)) { - return false; - } - - if (isInDoNotDisturbMode() && !allowCriticalInDND) { - return false; - } - - return true; - } - - function saveSettings() { - // In a real implementation, this would save to a config file - console.log("NotificationSettings: Settings saved"); - } - - function loadSettings() { - // In a real implementation, this would load from a config file - console.log("NotificationSettings: Settings loaded"); - } - - function resetToDefaults() { - notificationsEnabled = true; - soundEnabled = true; - persistNotifications = true; - defaultTimeout = 5000; - enableSmartGrouping = true; - autoExpandConversations = true; - replaceMediaNotifications = true; - maxStoredNotifications = 100; - notificationRetentionDays = 7; - showNotificationPopups = true; - showAppIcons = true; - showTimestamps = true; - enableInlineReply = true; - showActionButtons = true; - allowCriticalNotifications = true; - respectDoNotDisturb = true; - doNotDisturbMode = false; - doNotDisturbStart = "22:00"; - doNotDisturbEnd = "08:00"; - allowCriticalInDND = true; - notificationSound = "default"; - soundVolume = 0.7; - vibrationEnabled = false; - appSettings = {}; - - saveSettings(); - } - - Component.onCompleted: { - loadSettings(); - } -} \ No newline at end of file diff --git a/Widgets/StateLayer.qml b/Widgets/StateLayer.qml index 1f701196..e66810c1 100644 --- a/Widgets/StateLayer.qml +++ b/Widgets/StateLayer.qml @@ -7,7 +7,7 @@ MouseArea { property bool disabled: false property color stateColor: Theme.surfaceText - property real cornerRadius: parent?.radius ?? Appearance.rounding.normal + property real cornerRadius: parent?.radius ?? Theme.cornerRadius anchors.fill: parent cursorShape: disabled ? undefined : Qt.PointingHandCursor diff --git a/verify-notifications.sh b/verify-notifications.sh index 3aeb0cca..b51054d0 100755 --- a/verify-notifications.sh +++ b/verify-notifications.sh @@ -18,40 +18,34 @@ fi # Test 1: Basic notifications echo "📱 Test 1: Basic notifications" -notify-send -i preferences-desktop "Test App" "Basic notification message" +notify-send -h string:desktop-entry:org.gnome.Settings -i preferences-desktop "Settings" "Basic notification message" sleep 2 -# Test 2: Media notifications (should replace each other) -echo "🎵 Test 2: Media notifications (replacement behavior)" -notify-send -i audio-x-generic "Spotify" "Now Playing: Song 1 - Artist A" -sleep 2 -notify-send -i audio-x-generic "Spotify" "Now Playing: Song 2 - Artist B" -sleep 2 - -# Test 3: System notifications (grouped by category) -echo "🔋 Test 3: System notifications (grouped by category)" -notify-send -i battery "UPower" "Battery Low: 15% remaining" +# Test 2: Media notifications (should group under Spotify) +echo "🎵 Test 2: Media notifications (grouping)" +notify-send -h string:desktop-entry:spotify -i audio-x-generic "Spotify" "Now Playing: Song 1 - Artist A" sleep 1 -notify-send -i network-wired "NetworkManager" "Network Connected: WiFi connected" +notify-send -h string:desktop-entry:spotify -i audio-x-generic "Spotify" "Now Playing: Song 2 - Artist B" sleep 1 -notify-send -i system-software-update "System" "Updates Available: 5 packages can be updated" +notify-send -h string:desktop-entry:spotify -i audio-x-generic "Spotify" "Now Playing: Song 3 - Artist C" sleep 2 -# Test 4: Conversation notifications (should group and auto-expand) -echo "💬 Test 4: Conversation notifications (grouping)" -if command -v discord &> /dev/null; then - notify-send -i discord "Discord" "#general: User1 says Hello everyone!" - sleep 1 - notify-send -i discord "Discord" "#general: User2 says Hey there!" - sleep 1 - notify-send -i discord "Discord" "john_doe: Private message from John" -else - notify-send -i internet-chat "Discord" "#general: User1 says Hello everyone!" - sleep 1 - notify-send -i internet-chat "Discord" "#general: User2 says Hey there!" - sleep 1 - notify-send -i internet-chat "Discord" "john_doe: Private message from John" -fi +# Test 3: System notifications (separate groups) +echo "🔋 Test 3: System notifications (separate apps)" +notify-send -h string:desktop-entry:org.gnome.PowerStats -i battery "Power Manager" "Battery Low: 15% remaining" +sleep 1 +notify-send -h string:desktop-entry:org.gnome.NetworkDisplays -i network-wired "Network Manager" "WiFi Connected: HomeNetwork" +sleep 1 +notify-send -h string:desktop-entry:org.gnome.Software -i system-software-update "Software" "5 updates available" +sleep 2 + +# Test 4: Chat notifications (should group under Discord) +echo "💬 Test 4: Chat notifications (grouping)" +notify-send -h string:desktop-entry:discord -i internet-chat "Discord" "#general: User1 says Hello everyone!" +sleep 1 +notify-send -h string:desktop-entry:discord -i internet-chat "Discord" "#general: User2 says Hey there!" +sleep 1 +notify-send -h string:desktop-entry:discord -i internet-chat "Discord" "john_doe: Private message from John" sleep 2 # Test 5: Urgent notifications @@ -61,16 +55,16 @@ sleep 2 # Test 6: Notifications with actions (simulated) echo "⚡ Test 6: Action buttons" -notify-send -i system-upgrade "System Update" "Updates available - Click to install or remind later" +notify-send -h string:desktop-entry:org.gnome.Software -i system-upgrade "Software" "Updates available - Click to install or remind later" sleep 2 -# Test 7: Multiple apps generating notifications -echo "📊 Test 7: Multiple apps" -notify-send -i mail-message-new "Email" "You have 3 new emails" +# Test 7: Multiple different apps +echo "📊 Test 7: Multiple different apps" +notify-send -h string:desktop-entry:thunderbird -i mail-message-new "Thunderbird" "You have 3 new emails" sleep 0.5 -notify-send -i office-calendar "Calendar" "Daily standup in 5 minutes" +notify-send -h string:desktop-entry:org.gnome.Calendar -i office-calendar "Calendar" "Daily standup in 5 minutes" sleep 0.5 -notify-send -i folder-downloads "File Manager" "document.pdf downloaded" +notify-send -h string:desktop-entry:org.gnome.Nautilus -i folder-downloads "Files" "document.pdf downloaded" sleep 2 echo ""