1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 14:05:38 -05:00

Implement 2nd tier group expansion on notifications

This commit is contained in:
purian23
2025-07-18 23:21:10 -04:00
parent 1355a77fd0
commit 033d8b034b
4 changed files with 478 additions and 198 deletions

View File

@@ -402,26 +402,38 @@ PanelWindow {
width: parent.width width: parent.width
height: parent.height height: parent.height
readonly property bool isScreenshot: { readonly property bool isScreenshot: modelData.latestNotification.isScreenshot
// Check if this is a screenshot notification using NotificationService detection readonly property bool hasNotificationImage: modelData.latestNotification.image && modelData.latestNotification.image !== ""
return modelData.latestNotification.isScreenshot;
// Priority 1: Notification image using Quickshell IconImage
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: 20
clip: true
visible: parent.hasNotificationImage && centerNotificationImage.status === Image.Ready
color: "transparent"
IconImage {
id: centerNotificationImage
anchors.fill: parent
source: modelData.latestNotification.image || ""
}
} }
// Priority 2: Material Symbols icon for screenshots without notification images
// Use Material Symbols icon for screenshots with fallback
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "screenshot_monitor" name: "screenshot_monitor"
size: 20 size: 20
color: Theme.primaryText color: Theme.primaryText
visible: parent.isScreenshot visible: parent.isScreenshot && !parent.hasNotificationImage
} }
// Fallback to first letter for non-screenshot notifications // Priority 3: Fallback to first letter for other notifications
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: !parent.isScreenshot visible: !parent.hasNotificationImage && !parent.isScreenshot
text: { text: {
const appName = modelData.appName || "?"; const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase(); return appName.charAt(0).toUpperCase();
@@ -670,6 +682,71 @@ PanelWindow {
spacing: Theme.spacingM spacing: Theme.spacingM
visible: expanded visible: expanded
// 1st tier controls - moved above group header as per mockup
Item {
width: parent.width
height: 32
// Controls container - fixed position on right
Item {
width: 72
height: 32
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 32
height: 32
radius: 16
anchors.left: parent.left
color: collapseAreaTop.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "expand_less"
size: 18
color: Theme.surfaceText
}
MouseArea {
id: collapseAreaTop
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.toggleGroupExpansion(modelData.key)
}
}
Rectangle {
width: 32
height: 32
radius: 16
anchors.right: parent.right
color: dismissAllAreaTop.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "close"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: dismissAllAreaTop
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.dismissGroup(modelData.key)
}
}
}
}
// Group header in expanded view // Group header in expanded view
Item { Item {
width: parent.width width: parent.width
@@ -686,34 +763,50 @@ PanelWindow {
border.width: 1 border.width: 1
clip: true clip: true
readonly property bool hasNotificationImage: modelData.latestNotification.image && modelData.latestNotification.image !== ""
// Priority 1: Notification image using Quickshell IconImage
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: 16
clip: true
visible: parent.hasNotificationImage && expandedHeaderNotificationImage.status === Image.Ready
color: "transparent"
IconImage {
id: expandedHeaderNotificationImage
anchors.fill: parent
source: modelData.latestNotification.image || ""
}
}
// Priority 2: App icon for non-screenshots without notification images
IconImage { IconImage {
anchors.fill: parent anchors.fill: parent
anchors.margins: 4 anchors.margins: 4
source: { source: {
// Don't try to load icons for screenshots - let fallback handle them if (parent.hasNotificationImage || modelData.latestNotification.isScreenshot) {
const isScreenshot = modelData.latestNotification.isScreenshot;
if (isScreenshot) {
return ""; return "";
} }
return modelData.latestNotification.appIcon ? Quickshell.iconPath(modelData.latestNotification.appIcon, "") : ""; return modelData.latestNotification.appIcon ? Quickshell.iconPath(modelData.latestNotification.appIcon, "") : "";
} }
visible: status === Image.Ready visible: status === Image.Ready
} }
// Material Symbols icon for screenshots in expanded header // Priority 3: Material Symbols icon for screenshots without notification images
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "screenshot_monitor" name: "screenshot_monitor"
size: 16 size: 16
color: Theme.primaryText color: Theme.primaryText
visible: modelData.latestNotification.isScreenshot visible: modelData.latestNotification.isScreenshot && !parent.hasNotificationImage
} }
// Priority 4: Fallback text
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: !modelData.latestNotification.isScreenshot && (!modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === "") visible: !parent.hasNotificationImage && !modelData.latestNotification.isScreenshot && (!modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === "")
text: { text: {
const appName = modelData.appName || "?"; const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase(); return appName.charAt(0).toUpperCase();
@@ -725,74 +818,41 @@ PanelWindow {
} }
Text { // App name and count badge
Row {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 52 anchors.leftMargin: 52
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: modelData.appName spacing: Theme.spacingS
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
}
Item {
width: 72
height: 32
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 32
height: 32
radius: 16
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: 18
color: Theme.surfaceText
}
MouseArea {
id: collapseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.toggleGroupExpansion(modelData.key)
}
Text {
text: modelData.appName
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
anchors.verticalCenter: parent.verticalCenter
} }
// Message count badge when expanded
Rectangle { Rectangle {
width: 32 width: 20
height: 32 height: 20
radius: 16 radius: 10
anchors.right: parent.right color: Theme.primary
color: dismissAllArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" visible: modelData.count > 1
anchors.verticalCenter: parent.verticalCenter
DankIcon { Text {
anchors.centerIn: parent anchors.centerIn: parent
name: "close" text: modelData.count.toString()
size: 16 color: Theme.primaryText
color: Theme.surfaceText font.pixelSize: 10
font.weight: Font.Bold
} }
MouseArea {
id: dismissAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.dismissGroup(modelData.key)
}
} }
} }
} }
// Individual notifications // Individual notifications
@@ -833,18 +893,37 @@ PanelWindow {
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: 1 border.width: 1
// Material Symbols icon for individual screenshot notifications readonly property bool hasNotificationImage: modelData.image && modelData.image !== ""
// Priority 1: Notification image using Quickshell IconImage
Rectangle {
anchors.fill: parent
anchors.margins: 1
radius: 14
clip: true
visible: parent.hasNotificationImage && centerIndividualNotificationImage.status === Image.Ready
color: "transparent"
IconImage {
id: centerIndividualNotificationImage
anchors.fill: parent
source: modelData.image || ""
}
}
// Priority 2: Material Symbols icon for screenshots without notification images
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "screenshot_monitor" name: "screenshot_monitor"
size: 12 size: 12
color: Theme.primaryText color: Theme.primaryText
visible: modelData.isScreenshot visible: modelData.isScreenshot && !parent.hasNotificationImage
} }
// Priority 3: Fallback text
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: !modelData.isScreenshot visible: !parent.hasNotificationImage && !modelData.isScreenshot
text: { text: {
const appName = modelData.appName || "?"; const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase(); return appName.charAt(0).toUpperCase();
@@ -856,30 +935,64 @@ PanelWindow {
} }
Rectangle { // Individual controls - expand and dismiss buttons
width: 24 Row {
width: 50
height: 24 height: 24
radius: 12
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" spacing: 2
DankIcon { // Expand/collapse button for 2nd tier
anchors.centerIn: parent Rectangle {
name: "close" width: 24
size: 12 height: 24
color: Theme.surfaceVariantText radius: 12
color: individualExpandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
visible: modelData.body && modelData.body.length > 50 // Only show if body text is long enough
property bool isExpanded: NotificationService.expandedMessages[modelData.notification.id] || false
DankIcon {
anchors.centerIn: parent
name: parent.isExpanded ? "expand_less" : "expand_more"
size: 12
color: Theme.surfaceVariantText
}
MouseArea {
id: individualExpandArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.toggleMessageExpansion(modelData.notification.id)
}
} }
MouseArea { // Individual dismiss button
id: individualDismissArea Rectangle {
width: 24
height: 24
radius: 12
color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
anchors.fill: parent DankIcon {
hoverEnabled: true anchors.centerIn: parent
cursorShape: Qt.PointingHandCursor name: "close"
onClicked: NotificationService.dismissNotification(modelData) size: 12
color: Theme.surfaceVariantText
}
MouseArea {
id: individualDismissArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.dismissNotification(modelData)
}
} }
} }
Column { Column {
@@ -888,27 +1001,42 @@ PanelWindow {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 44 anchors.leftMargin: 44
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 36 anchors.rightMargin: 60 // More space for expanded controls
anchors.top: parent.top anchors.top: parent.top
spacing: Theme.spacingXS spacing: Theme.spacingXS
property bool isMessageExpanded: NotificationService.expandedMessages[modelData.notification.id] || false
// Title • timestamp format
Text { Text {
text: modelData.summary text: {
const summary = modelData.summary || "";
const timeStr = modelData.timeStr || "";
if (summary && timeStr) {
return summary + " • " + timeStr;
} else if (summary) {
return summary;
} else {
return "Message • " + timeStr;
}
}
color: Theme.surfaceText color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1
} }
// Body text with expandable behavior
Text { Text {
text: modelData.body text: modelData.body
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
width: parent.width width: parent.width
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
maximumLineCount: 3 maximumLineCount: parent.isMessageExpanded ? -1 : 3 // Unlimited when expanded, 3 when collapsed (more space in center)
elide: Text.ElideRight elide: parent.isMessageExpanded ? Text.ElideNone : Text.ElideRight
visible: text.length > 0 visible: text.length > 0
} }

View File

@@ -173,23 +173,37 @@ PanelWindow {
height: parent.height height: parent.height
readonly property bool isScreenshot: modelData.latestNotification.isScreenshot readonly property bool isScreenshot: modelData.latestNotification.isScreenshot
readonly property bool hasNotificationImage: modelData.latestNotification.image && modelData.latestNotification.image !== ""
// Priority 1: Notification image using Quickshell IconImage (handles qs://image-X URIs)
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: 20
clip: true
visible: parent.hasNotificationImage && notificationImage.status === Image.Ready
color: "transparent"
IconImage {
id: notificationImage
anchors.fill: parent
source: modelData.latestNotification.image || ""
}
}
// Priority 2: Material Symbols icon for screenshots (when no image available)
// Use Material Symbols icon for screenshots with fallback
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "screenshot_monitor" name: "screenshot_monitor"
size: 24 size: 24
color: Theme.primaryText color: Theme.primaryText
visible: parent.isScreenshot visible: parent.isScreenshot && !parent.hasNotificationImage
} }
// Fallback to first letter for non-screenshot notifications // Priority 3: Fallback to first letter for other notifications
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: !parent.isScreenshot visible: !parent.hasNotificationImage && !parent.isScreenshot
text: { text: {
const appName = modelData.appName || "?"; const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase(); return appName.charAt(0).toUpperCase();
@@ -447,71 +461,10 @@ PanelWindow {
spacing: Theme.spacingM spacing: Theme.spacingM
visible: expanded visible: expanded
// Group header with fixed anchored positioning // 1st tier controls - moved above group header as per mockup
Item { Item {
width: parent.width width: parent.width
height: 48 height: 32
// Round app icon - fixed position on left
Rectangle {
width: 40
height: 40
radius: 20
anchors.left: parent.left
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.3)
border.width: 1
clip: true
IconImage {
anchors.fill: parent
anchors.margins: 4
source: {
// Don't try to load icons for screenshots - let fallback handle them
if (modelData.latestNotification.isScreenshot) {
return "";
}
return modelData.latestNotification.appIcon ? Quickshell.iconPath(modelData.latestNotification.appIcon, "") : "";
}
visible: status === Image.Ready
}
// Material Symbols icon for screenshots in expanded view
DankIcon {
anchors.centerIn: parent
name: "screenshot_monitor"
size: 16
color: Theme.primaryText
visible: modelData.latestNotification.isScreenshot
}
// Fallback for expanded view
Text {
anchors.centerIn: parent
visible: !modelData.latestNotification.isScreenshot && (!modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === "")
text: {
const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 16
font.weight: Font.Bold
color: Theme.primaryText
}
}
// App name and count badge - centered area
Text {
anchors.left: parent.left
anchors.leftMargin: 52
anchors.verticalCenter: parent.verticalCenter
text: modelData.appName
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
}
// Controls container - fixed position on right // Controls container - fixed position on right
Item { Item {
@@ -525,7 +478,7 @@ PanelWindow {
height: 32 height: 32
radius: 16 radius: 16
anchors.left: parent.left anchors.left: parent.left
color: collapseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" color: collapseAreaTop.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -535,7 +488,7 @@ PanelWindow {
} }
MouseArea { MouseArea {
id: collapseArea id: collapseAreaTop
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
@@ -554,7 +507,7 @@ PanelWindow {
height: 32 height: 32
radius: 16 radius: 16
anchors.right: parent.right anchors.right: parent.right
color: dismissAllArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" color: dismissAllAreaTop.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -564,7 +517,7 @@ PanelWindow {
} }
MouseArea { MouseArea {
id: dismissAllArea id: dismissAllAreaTop
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
@@ -575,6 +528,114 @@ PanelWindow {
} }
} }
}
// Group header with fixed anchored positioning
Item {
width: parent.width
height: 48
// Round app icon - fixed position on left
Rectangle {
width: 40
height: 40
radius: 20
anchors.left: parent.left
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.3)
border.width: 1
clip: true
readonly property bool hasNotificationImage: modelData.latestNotification.image && modelData.latestNotification.image !== ""
// Priority 1: Notification image using Quickshell IconImage
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: 16
clip: true
visible: parent.hasNotificationImage && expandedNotificationImage.status === Image.Ready
color: "transparent"
IconImage {
id: expandedNotificationImage
anchors.fill: parent
source: modelData.latestNotification.image || ""
}
}
// Priority 2: App icon for non-screenshots without notification images
IconImage {
anchors.fill: parent
anchors.margins: 4
source: {
if (parent.hasNotificationImage || modelData.latestNotification.isScreenshot) {
return "";
}
return modelData.latestNotification.appIcon ? Quickshell.iconPath(modelData.latestNotification.appIcon, "") : "";
}
visible: status === Image.Ready
}
// Priority 3: Material Symbols icon for screenshots without notification images
DankIcon {
anchors.centerIn: parent
name: "screenshot_monitor"
size: 16
color: Theme.primaryText
visible: modelData.latestNotification.isScreenshot && !parent.hasNotificationImage
}
// Priority 4: Fallback text
Text {
anchors.centerIn: parent
visible: !parent.hasNotificationImage && !modelData.latestNotification.isScreenshot && (!modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === "")
text: {
const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 16
font.weight: Font.Bold
color: Theme.primaryText
}
}
// App name and count badge - centered area
Row {
anchors.left: parent.left
anchors.leftMargin: 52
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Text {
text: modelData.appName
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
anchors.verticalCenter: parent.verticalCenter
}
// Message count badge when expanded
Rectangle {
width: 20
height: 20
radius: 10
color: Theme.primary
visible: modelData.count > 1
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: modelData.count.toString()
color: Theme.primaryText
font.pixelSize: 10
font.weight: Font.Bold
}
}
}
} }
@@ -619,33 +680,50 @@ PanelWindow {
border.width: 1 border.width: 1
clip: true clip: true
readonly property bool hasNotificationImage: modelData.image && modelData.image !== ""
// Priority 1: Notification image using Quickshell IconImage
Rectangle {
anchors.fill: parent
anchors.margins: 1
radius: 14
clip: true
visible: parent.hasNotificationImage && individualNotificationImage.status === Image.Ready
color: "transparent"
IconImage {
id: individualNotificationImage
anchors.fill: parent
source: modelData.image || ""
}
}
// Priority 2: App icon for non-screenshots without notification images
IconImage { IconImage {
anchors.fill: parent anchors.fill: parent
anchors.margins: 3 anchors.margins: 3
source: { source: {
// Don't try to load icons for screenshots if (parent.hasNotificationImage || modelData.isScreenshot) {
if (modelData.isScreenshot) {
return ""; return "";
} }
return modelData.appIcon ? Quickshell.iconPath(modelData.appIcon, "") : ""; return modelData.appIcon ? Quickshell.iconPath(modelData.appIcon, "") : "";
} }
visible: status === Image.Ready visible: status === Image.Ready
} }
// Material Symbols icon for individual screenshot notifications // Priority 3: Material Symbols icon for screenshots without notification images
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "screenshot_monitor" name: "screenshot_monitor"
size: 12 size: 12
color: Theme.primaryText color: Theme.primaryText
visible: modelData.isScreenshot visible: modelData.isScreenshot && !parent.hasNotificationImage
} }
// Fallback for individual notifications // Priority 4: Fallback text
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
visible: !modelData.isScreenshot && (!modelData.appIcon || modelData.appIcon === "") visible: !parent.hasNotificationImage && !modelData.isScreenshot && (!modelData.appIcon || modelData.appIcon === "")
text: { text: {
const appName = modelData.appName || "?"; const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase(); return appName.charAt(0).toUpperCase();
@@ -657,31 +735,64 @@ PanelWindow {
} }
// Individual dismiss button - fixed position on right // Individual controls - expand and dismiss buttons
Rectangle { Row {
width: 24 width: 50
height: 24 height: 24
radius: 12
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent" spacing: 2
DankIcon { // Expand/collapse button for 2nd tier
anchors.centerIn: parent Rectangle {
name: "close" width: 24
size: 12 height: 24
color: Theme.surfaceVariantText radius: 12
color: individualExpandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
visible: modelData.body && modelData.body.length > 50 // Only show if body text is long enough
property bool isExpanded: NotificationService.expandedMessages[modelData.notification.id] || false
DankIcon {
anchors.centerIn: parent
name: parent.isExpanded ? "expand_less" : "expand_more"
size: 12
color: Theme.surfaceVariantText
}
MouseArea {
id: individualExpandArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.toggleMessageExpansion(modelData.notification.id)
}
} }
MouseArea { // Individual dismiss button
id: individualDismissArea Rectangle {
width: 24
height: 24
radius: 12
color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
anchors.fill: parent DankIcon {
hoverEnabled: true anchors.centerIn: parent
cursorShape: Qt.PointingHandCursor name: "close"
onClicked: NotificationService.dismissNotification(modelData) size: 12
color: Theme.surfaceVariantText
}
MouseArea {
id: individualDismissArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.dismissNotification(modelData)
}
} }
} }
// Notification content - fills space between icon and dismiss button // Notification content - fills space between icon and dismiss button
@@ -691,29 +802,42 @@ PanelWindow {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 44 anchors.leftMargin: 44
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 36 anchors.rightMargin: 60 // More space for expanded controls
anchors.top: parent.top anchors.top: parent.top
spacing: Theme.spacingXS spacing: Theme.spacingXS
// Title and timestamp property bool isMessageExpanded: NotificationService.expandedMessages[modelData.notification.id] || false
// Title • timestamp format
Text { Text {
text: modelData.summary text: {
const summary = modelData.summary || "";
const timeStr = modelData.timeStr || "";
if (summary && timeStr) {
return summary + " • " + timeStr;
} else if (summary) {
return summary;
} else {
return "Message • " + timeStr;
}
}
color: Theme.surfaceText color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1
} }
// Body text // Body text with expandable behavior
Text { Text {
text: modelData.body text: modelData.body
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
width: parent.width width: parent.width
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
maximumLineCount: 2 maximumLineCount: parent.isMessageExpanded ? -1 : 2 // Unlimited when expanded, 2 when collapsed
elide: Text.ElideRight elide: parent.isMessageExpanded ? Text.ElideNone : Text.ElideRight
visible: text.length > 0 visible: text.length > 0
} }

View File

@@ -16,6 +16,7 @@ Singleton {
readonly property var groupedPopups: getGroupedPopups() readonly property var groupedPopups: getGroupedPopups()
property var expandedGroups: ({}) // Track which groups are expanded property var expandedGroups: ({}) // Track which groups are expanded
property var expandedMessages: ({}) // Track which individual messages are expanded
NotificationServer { NotificationServer {
id: server id: server
@@ -403,6 +404,15 @@ Singleton {
} }
} }
function toggleMessageExpansion(messageId) {
let newExpandedMessages = {};
for (const key in expandedMessages) {
newExpandedMessages[key] = expandedMessages[key];
}
newExpandedMessages[messageId] = !newExpandedMessages[messageId];
expandedMessages = newExpandedMessages;
}
function getGroupTitle(group) { function getGroupTitle(group) {
if (group.count === 1) { if (group.count === 1) {
return group.latestNotification.summary; return group.latestNotification.summary;

18
test_notifications.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Test script for the new 2nd tier notification system
echo "Testing 2nd tier notification system..."
# Send a few test notifications to create a group
notify-send "Test App" "Short message 1"
sleep 1
notify-send "Test App" "This is a much longer message that should trigger the expand/collapse functionality for individual messages within the notification group system"
sleep 1
notify-send "Test App" "Message 3 with some content"
echo "Test notifications sent. Check the notification popup and center for:"
echo "1. 1st tier controls moved above group header"
echo "2. Message count badge next to app name when expanded"
echo "3. 'title • timestamp' format for individual messages"
echo "4. Expand/collapse buttons for long individual messages"