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

Notification updates

This commit is contained in:
purian23
2025-07-14 10:51:17 -04:00
parent cf5c26522b
commit 5c5653a41a
4 changed files with 375 additions and 116 deletions

View File

@@ -26,6 +26,30 @@ Singleton {
`, root)
}
// Format timestamp for display
function formatTimestamp(timestamp) {
if (!timestamp) return ""
const now = new Date()
const notifTime = new Date(timestamp)
const diffMs = now.getTime() - notifTime.getTime()
const diffMinutes = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMinutes < 1) {
return "now"
} else if (diffMinutes < 60) {
return `${diffMinutes}m ago`
} else if (diffHours < 24) {
return `${diffHours}h ago`
} else if (diffDays < 7) {
return `${diffDays}d ago`
} else {
return notifTime.toLocaleDateString()
}
}
// Add a new notification to the appropriate group
function addNotification(notificationObj) {
if (!notificationObj || !notificationObj.appName) {

View File

@@ -28,6 +28,18 @@ PanelWindow {
bottom: true
}
// Timer to update timestamps periodically
Timer {
id: timestampUpdateTimer
interval: 60000 // Update every minute
running: visible
repeat: true
onTriggered: {
// Force model refresh to update timestamps
groupedNotificationListView.model = NotificationGroupingService.groupedNotifications
}
}
Rectangle {
width: 400
height: 500
@@ -156,11 +168,18 @@ PanelWindow {
width: parent.width
height: parent.height - 120
clip: true
contentWidth: -1 // Fit to width
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ListView {
id: groupedNotificationListView
model: NotificationGroupingService.groupedNotifications
spacing: Theme.spacingM
interactive: true
boundsBehavior: Flickable.StopAtBounds
flickDeceleration: 1500
maximumFlickVelocity: 2000
delegate: Column {
width: groupedNotificationListView.width
@@ -178,124 +197,137 @@ PanelWindow {
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
// App Icon
Rectangle {
width: 32
height: 32
radius: width / 2
color: Theme.primaryContainer
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
// App Icon
Rectangle {
width: 32
height: 32
radius: width / 2
color: Theme.primaryContainer
anchors.verticalCenter: parent.verticalCenter
// Material icon fallback
Loader {
active: !model.appIcon || model.appIcon === ""
anchors.fill: parent
sourceComponent: Text {
anchors.centerIn: parent
text: "apps"
font.family: Theme.iconFont
font.pixelSize: 16
color: Theme.primaryText
}
}
// App icon
Loader {
active: model.appIcon && model.appIcon !== ""
// Material icon fallback
Loader {
active: !model.appIcon || model.appIcon === ""
anchors.fill: parent
sourceComponent: Text {
anchors.centerIn: parent
sourceComponent: IconImage {
width: 24
height: 24
asynchronous: true
source: {
if (!model.appIcon) return ""
if (model.appIcon.startsWith("file://") || model.appIcon.startsWith("/")) {
return model.appIcon
}
return Quickshell.iconPath(model.appIcon, "image-missing")
}
}
text: "apps"
font.family: Theme.iconFont
font.pixelSize: 16
color: Theme.primaryText
}
}
// App Name and Summary
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 100
spacing: 2
// App icon
Loader {
active: model.appIcon && model.appIcon !== ""
anchors.centerIn: parent
sourceComponent: IconImage {
width: 24
height: 24
asynchronous: true
source: {
if (!model.appIcon) return ""
if (model.appIcon.startsWith("file://") || model.appIcon.startsWith("/")) {
return model.appIcon
}
return Quickshell.iconPath(model.appIcon, "image-missing")
}
}
}
}
// App Name and Summary
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM + 32 + Theme.spacingM // Icon + spacing
anchors.right: parent.right
anchors.rightMargin: 80 // Space for buttons
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Row {
width: parent.width
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
Text {
text: model.appName || "App"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
// Notification count badge
Rectangle {
width: Math.max(countText.width + 8, 20)
height: 20
radius: 10
color: Theme.primary
visible: model.totalCount > 1
anchors.verticalCenter: parent.verticalCenter
Text {
text: model.appName || "App"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
id: countText
anchors.centerIn: parent
text: model.totalCount.toString()
font.pixelSize: Theme.fontSizeSmall
color: Theme.primaryText
font.weight: Font.Medium
}
// Notification count badge
Rectangle {
width: Math.max(countText.width + 8, 20)
height: 20
radius: 10
color: Theme.primary
visible: model.totalCount > 1
anchors.verticalCenter: parent.verticalCenter
Text {
id: countText
anchors.centerIn: parent
text: model.totalCount.toString()
font.pixelSize: Theme.fontSizeSmall
color: Theme.primaryText
font.weight: Font.Medium
}
}
}
Text {
text: model.latestNotification ?
(model.latestNotification.summary || model.latestNotification.body || "") : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
}
// Expand/Collapse Icon
Rectangle {
width: 32
height: 32
radius: 16
anchors.verticalCenter: parent.verticalCenter
color: groupHeaderArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
"transparent"
Text {
text: model.latestNotification ?
(model.latestNotification.summary || model.latestNotification.body || "") : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
}
// Expand/Collapse Icon
Rectangle {
id: expandCollapseButton
width: 32
height: 32
radius: 16
anchors.right: parent.right
anchors.rightMargin: 40 // More space from close button
anchors.verticalCenter: parent.verticalCenter
color: expandButtonArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
"transparent"
Text {
anchors.centerIn: parent
text: isExpanded ? "expand_less" : "expand_more"
font.family: Theme.iconFont
font.pixelSize: 20
color: expandButtonArea.containsMouse ? Theme.primary : Theme.surfaceText
Text {
anchors.centerIn: parent
text: isExpanded ? "expand_less" : "expand_more"
font.family: Theme.iconFont
font.pixelSize: 20
color: groupHeaderArea.containsMouse ? Theme.primary : Theme.surfaceText
Behavior on rotation {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
Behavior on rotation {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
id: expandButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NotificationGroupingService.toggleGroupExpansion(index)
}
}
}
// Close group button
@@ -329,12 +361,28 @@ PanelWindow {
}
}
// Timestamp positioned under close button
Text {
id: timestampText
text: model.latestNotification ?
NotificationGroupingService.formatTimestamp(model.latestNotification.timestamp) : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: 6
anchors.bottomMargin: 6
visible: text.length > 0
}
MouseArea {
id: groupHeaderArea
anchors.fill: parent
anchors.rightMargin: 32
anchors.rightMargin: 76 // Exclude both expand and close button areas
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: false
propagateComposedEvents: true
onClicked: {
NotificationGroupingService.toggleGroupExpansion(index)
@@ -350,13 +398,30 @@ PanelWindow {
}
// Expanded Notifications List
Loader {
Item {
width: parent.width
active: isExpanded
height: isExpanded ? expandedContent.height : 0
clip: true
sourceComponent: Column {
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: expandedContent
width: parent.width
spacing: Theme.spacingXS
opacity: isExpanded ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Repeater {
model: groupData.notifications
@@ -525,6 +590,13 @@ PanelWindow {
elide: Text.ElideRight
visible: text.length > 0
}
Text {
text: NotificationGroupingService.formatTimestamp(model.timestamp)
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: text.length > 0
}
}
}
@@ -534,6 +606,8 @@ PanelWindow {
anchors.rightMargin: 32
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: false
propagateComposedEvents: true
onClicked: {
if (model && root.handleNotificationClick) {

View File

@@ -43,10 +43,114 @@ PanelWindow {
opacity: root.showNotificationPopup ? 1.0 : 0.0
// Transform for swipe animations
transform: Translate {
id: swipeTransform
x: 0
}
Behavior on opacity {
NumberAnimation { duration: 200; easing.type: Easing.OutQuad }
}
// Drag area for swipe gestures
DragHandler {
id: dragHandler
target: null // We'll handle the transform manually
acceptedDevices: PointerDevice.TouchScreen | PointerDevice.Mouse
property real startX: 0
property real currentDelta: 0
property bool isDismissing: false
onActiveChanged: {
if (active) {
startX = centroid.position.x
currentDelta = 0
isDismissing = false
} else {
// Handle end of drag
let deltaX = centroid.position.x - startX
if (Math.abs(deltaX) > 80) { // Threshold for swipe action
if (deltaX > 0) {
// Swipe right - open notification history
swipeOpenHistory()
} else {
// Swipe left - dismiss notification
swipeDismiss()
}
} else {
// Snap back to original position
snapBack()
}
}
}
onCentroidChanged: {
if (active) {
let deltaX = centroid.position.x - startX
currentDelta = deltaX
// Limit swipe distance and add resistance
let maxDistance = 120
let resistance = 0.6
if (Math.abs(deltaX) > maxDistance) {
deltaX = deltaX > 0 ? maxDistance : -maxDistance
}
swipeTransform.x = deltaX * resistance
// Visual feedback - reduce opacity when swiping left (dismiss)
if (deltaX < 0) {
popupContainer.opacity = Math.max(0.3, 1.0 - Math.abs(deltaX) / 150)
} else {
popupContainer.opacity = Math.max(0.7, 1.0 - Math.abs(deltaX) / 200)
}
}
}
function swipeOpenHistory() {
// Animate to the right and open history
swipeAnimation.to = 400
swipeAnimation.onFinished = function() {
root.notificationHistoryVisible = true
Utils.hideNotificationPopup()
snapBack()
}
swipeAnimation.start()
}
function swipeDismiss() {
// Animate to the left and dismiss
swipeAnimation.to = -400
swipeAnimation.onFinished = function() {
Utils.hideNotificationPopup()
snapBack()
}
swipeAnimation.start()
}
function snapBack() {
swipeAnimation.to = 0
swipeAnimation.onFinished = function() {
popupContainer.opacity = Qt.binding(() => root.showNotificationPopup ? 1.0 : 0.0)
}
swipeAnimation.start()
}
}
// Swipe animation
NumberAnimation {
id: swipeAnimation
target: swipeTransform
property: "x"
duration: 200
easing.type: Easing.OutCubic
}
// Tap area for notification interaction
MouseArea {
anchors.fill: parent
anchors.rightMargin: 36 // Don't overlap with close button
@@ -58,15 +162,9 @@ PanelWindow {
console.log("Popup clicked!")
if (root.activeNotification) {
root.handleNotificationClick(root.activeNotification)
// Remove notification from history entirely
for (let i = 0; i < notificationHistory.count; i++) {
if (notificationHistory.get(i).id === root.activeNotification.id) {
notificationHistory.remove(i)
break
}
}
// Don't remove from history - just hide popup
}
// Always hide popup after click
// Hide popup but keep in history
Utils.hideNotificationPopup()
mouse.accepted = true // Prevent event propagation
}
@@ -110,11 +208,77 @@ PanelWindow {
}
}
// Small dismiss button - bottom right corner
Rectangle {
width: 60
height: 18
radius: 9
color: dismissButtonArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
border.color: dismissButtonArea.containsMouse ?
Theme.primary :
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
border.width: 1
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: 12
anchors.bottomMargin: 10
Row {
anchors.centerIn: parent
spacing: 4
Text {
text: "archive"
font.family: Theme.iconFont
font.pixelSize: 10
color: dismissButtonArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Dismiss"
font.pixelSize: 10
color: dismissButtonArea.containsMouse ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: dismissButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// Just hide the popup, keep in history
Utils.hideNotificationPopup()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Content layout
Row {
anchors.fill: parent
anchors.margins: 12
anchors.rightMargin: 32
anchors.bottomMargin: 6 // Reduced bottom margin to account for dismiss button
spacing: 12
// Notification icon based on EXAMPLE NotificationAppIcon pattern

View File

@@ -1,6 +1,3 @@
import QtQuick 6.5
import QtQuick.Controls 6.5
TopBar 1.0 TopBar/TopBar.qml
TrayMenuPopup 1.0 TrayMenuPopup.qml
NotificationPopup 1.0 NotificationPopup.qml