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

notification: general improvements to popups

This commit is contained in:
bbedward
2025-07-26 13:25:39 -04:00
parent ec1bf7c110
commit 484a947127
2 changed files with 189 additions and 92 deletions

View File

@@ -19,7 +19,7 @@ PanelWindow {
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent" color: "transparent"
implicitWidth: 400 implicitWidth: 400
implicitHeight: 116 implicitHeight: 122
anchors { anchors {
top: true top: true
@@ -138,13 +138,12 @@ PanelWindow {
anchors.right: parent.right anchors.right: parent.right
anchors.topMargin: 12 anchors.topMargin: 12
anchors.leftMargin: 16 anchors.leftMargin: 16
anchors.rightMargin: 16 anchors.rightMargin: 56
height: 86 height: 98
Rectangle { Rectangle {
id: iconContainer id: iconContainer
readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== "" readonly property bool hasNotificationImage: notificationData && notificationData.image && notificationData.image !== ""
readonly property bool appIconIsImage: notificationData && notificationData.appIcon && (notificationData.appIcon.startsWith("file://") || notificationData.appIcon.startsWith("http://") || notificationData.appIcon.startsWith("https://"))
property alias iconImage: iconImage property alias iconImage: iconImage
width: 55 width: 55
@@ -196,89 +195,169 @@ PanelWindow {
id: textContainer id: textContainer
anchors.left: iconContainer.right anchors.left: iconContainer.right
anchors.leftMargin: 12 anchors.leftMargin: 12
anchors.right: closeButton.left anchors.right: parent.right
anchors.rightMargin: 8 anchors.rightMargin: 0
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 8 anchors.bottomMargin: 8
color: "transparent" color: "transparent"
Column { Item {
width: parent.width width: parent.width
spacing: 2 height: parent.height
anchors.verticalCenter: parent.verticalCenter anchors.top: parent.top
anchors.topMargin: -4
Text { Column {
width: parent.width width: parent.width
text: { spacing: 2
if (!notificationData) return "";
const appName = notificationData.appName || "";
const timeStr = notificationData.timeStr || "";
if (timeStr.length > 0)
return appName + " • " + timeStr;
else
return appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
Text { Text {
text: notificationData ? (notificationData.summary || "") : "" width: parent.width
color: Theme.surfaceText text: {
font.pixelSize: Theme.fontSizeMedium if (!notificationData) return "";
font.weight: Font.Medium const appName = notificationData.appName || "";
width: parent.width const timeStr = notificationData.timeStr || "";
elide: Text.ElideRight if (timeStr.length > 0)
maximumLineCount: 1 return appName + " • " + timeStr;
visible: text.length > 0 else
} return appName;
}
Text { color: Theme.surfaceVariantText
property bool hasUrls: { font.pixelSize: Theme.fontSizeSmall
if (!notificationData || !notificationData.body) return false; font.weight: Font.Medium
const urlRegex = /(https?:\/\/[^\s]+)/g; elide: Text.ElideRight
return urlRegex.test(notificationData.body); maximumLineCount: 1
} }
text: { Text {
if (!notificationData || !notificationData.body) return ""; text: notificationData ? (notificationData.summary || "") : ""
let bodyText = notificationData.body; color: Theme.surfaceText
if (bodyText.length > 105) font.pixelSize: Theme.fontSizeMedium
bodyText = bodyText.substring(0, 102) + "..."; font.weight: Font.Medium
width: parent.width
const urlRegex = /(https?:\/\/[^\s]+)/g; elide: Text.ElideRight
return bodyText.replace(urlRegex, '<a href="$1" style="color: ' + Theme.primary + '; text-decoration: underline;">$1</a>'); maximumLineCount: 1
visible: text.length > 0
} }
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall Text {
width: parent.width text: notificationData ? (notificationData.body || "") : ""
elide: Text.ElideRight color: Theme.surfaceVariantText
maximumLineCount: 2 font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap width: parent.width
visible: text.length > 0 elide: Text.ElideRight
textFormat: Text.RichText maximumLineCount: 2
onLinkActivated: function(link) { wrapMode: Text.WordWrap
Qt.openUrlExternally(link); visible: text.length > 0
textFormat: Text.PlainText
} }
} }
} }
} }
DankActionButton {
id: closeButton }
anchors.right: parent.right
anchors.top: parent.top DankActionButton {
iconName: "close" id: closeButton
iconSize: 14 anchors.right: parent.right
buttonSize: 20 anchors.top: parent.top
z: 15 anchors.topMargin: 12
anchors.rightMargin: 16
iconName: "close"
iconSize: 18
buttonSize: 28
z: 15
onClicked: {
if (notificationData)
notificationData.popup = false;
}
}
Row {
anchors.right: dismissButton.left
anchors.rightMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
spacing: 8
z: 20
Repeater {
model: notificationData ? (notificationData.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
acceptedButtons: Qt.LeftButton
onEntered: parent.isHovered = true
onExited: parent.isHovered = false
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke();
}
if (notificationData) {
notificationData.popup = false;
}
}
}
}
}
}
Rectangle {
id: dismissButton
property bool isHovered: false
anchors.right: parent.right
anchors.rightMargin: 16
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
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"
z: 20
Text {
id: dismissText
text: "Dismiss"
color: dismissButton.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
acceptedButtons: Qt.LeftButton
onEntered: dismissButton.isHovered = true
onExited: dismissButton.isHovered = false
onClicked: { onClicked: {
if (notificationData) if (notificationData) {
notificationData.popup = false; NotificationService.dismissNotification(notificationData);
}
} }
} }
} }
@@ -289,7 +368,7 @@ PanelWindow {
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
propagateComposedEvents: true propagateComposedEvents: true
z: 0 z: -1
onEntered: { onEntered: {
if (notificationData && notificationData.timer) if (notificationData && notificationData.timer)
notificationData.timer.stop(); notificationData.timer.stop();

View File

@@ -21,7 +21,8 @@ Singleton {
property bool addGateBusy: false property bool addGateBusy: false
property int enterAnimMs: 400 property int enterAnimMs: 400
property int seqCounter: 0 property int seqCounter: 0
property bool bulkDismissing: false
Timer { Timer {
id: addGate id: addGate
interval: enterAnimMs + 50 interval: enterAnimMs + 50
@@ -34,6 +35,7 @@ Singleton {
readonly property var groupedNotifications: getGroupedNotifications() readonly property var groupedNotifications: getGroupedNotifications()
readonly property var groupedPopups: getGroupedPopups() readonly property var groupedPopups: getGroupedPopups()
property var expandedGroups: ({}) property var expandedGroups: ({})
property var expandedMessages: ({}) property var expandedMessages: ({})
property bool popupsDisabled: false property bool popupsDisabled: false
@@ -149,24 +151,20 @@ Singleton {
function onDropped(): void { function onDropped(): void {
const notifIndex = root.notifications.indexOf(wrapper); const notifIndex = root.notifications.indexOf(wrapper);
const allIndex = root.allWrappers.indexOf(wrapper); const allIndex = root.allWrappers.indexOf(wrapper);
if (allIndex !== -1) root.allWrappers.splice(allIndex, 1);
if (notifIndex !== -1) root.notifications.splice(notifIndex, 1);
if (root.bulkDismissing) return;
const groupKey = getGroupKey(wrapper);
const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey);
if (allIndex !== -1) { // Only collapse the group if there's 1 or fewer notifications left
root.allWrappers.splice(allIndex, 1); if (remainingInGroup.length <= 1) {
clearGroupExpansionState(groupKey);
} }
if (notifIndex !== -1) { cleanupExpansionStates();
const groupKey = getGroupKey(wrapper);
root.notifications.splice(notifIndex, 1);
const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey);
if (remainingInGroup.length === 0) {
clearGroupExpansionState(groupKey);
} else if (remainingInGroup.length === 1) {
clearGroupExpansionState(groupKey);
}
cleanupExpansionStates();
}
} }
function onAboutToDestroy(): void { function onAboutToDestroy(): void {
@@ -182,17 +180,34 @@ Singleton {
// Helper functions // Helper functions
function clearAllNotifications() { function clearAllNotifications() {
// Actually dismiss all notifications from center bulkDismissing = true;
const notificationsCopy = [...root.notifications]; popupsDisabled = true;
notificationsCopy.forEach(notif => { addGate.stop();
notif.notification.dismiss(); addGateBusy = false;
}); notificationQueue = [];
// Clear all expansion states
for (const w of visibleNotifications) w.popup = false;
visibleNotifications = [];
const toDismiss = notifications.slice();
if (notifications.length) notifications.splice(0, notifications.length);
expandedGroups = {}; expandedGroups = {};
expandedMessages = {}; expandedMessages = {};
for (let i = 0; i < toDismiss.length; ++i) {
const w = toDismiss[i];
if (w && w.notification) {
try { w.notification.dismiss(); } catch (e) { /* ignore */ }
}
}
bulkDismissing = false;
popupsDisabled = false;
} }
function dismissNotification(wrapper) { function dismissNotification(wrapper) {
wrapper.popup = false;
wrapper.notification.dismiss(); wrapper.notification.dismiss();
} }
@@ -367,6 +382,7 @@ Singleton {
} }
} }
function clearGroupExpansionState(groupKey) { function clearGroupExpansionState(groupKey) {
let newExpandedGroups = {}; let newExpandedGroups = {};
for (const key in expandedGroups) { for (const key in expandedGroups) {
@@ -377,6 +393,7 @@ Singleton {
expandedGroups = newExpandedGroups; expandedGroups = newExpandedGroups;
} }
function cleanupExpansionStates() { function cleanupExpansionStates() {
const currentGroupKeys = new Set(groupedNotifications.map(g => g.key)); const currentGroupKeys = new Set(groupedNotifications.map(g => g.key));
const currentMessageIds = new Set(); const currentMessageIds = new Set();
@@ -464,6 +481,7 @@ Singleton {
notif.body.toLowerCase().includes(searchLower) notif.body.toLowerCase().includes(searchLower)
); );
} }
Component.onCompleted: { Component.onCompleted: {
cleanupPersistentStorage(); cleanupPersistentStorage();
} }