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

Redesign of the notification center

This commit is contained in:
purian23
2025-07-14 20:32:06 -04:00
parent 6f1e23437c
commit 56b801c294
8 changed files with 1484 additions and 102 deletions

View File

@@ -6,8 +6,8 @@ pragma ComponentBehavior: Bound
Singleton { Singleton {
id: root id: root
// Grouped notifications model // Grouped notifications model - initialize as ListModel directly
property var groupedNotifications: ListModel {} property ListModel groupedNotifications: ListModel {}
// Total count of all notifications across all groups // Total count of all notifications across all groups
property int totalCount: 0 property int totalCount: 0
@@ -15,16 +15,35 @@ Singleton {
// Map to track group indices by app name for efficient lookups // Map to track group indices by app name for efficient lookups
property var appGroupMap: ({}) property var appGroupMap: ({})
// Debounce timer for sorting
property bool _sortDirty: false
Timer {
id: sortTimer
interval: 50 // 50ms debounce interval
onTriggered: {
if (_sortDirty) {
sortGroupsByPriority()
_sortDirty = false
}
}
}
// Configuration // Configuration
property int maxNotificationsPerGroup: 10 property int maxNotificationsPerGroup: 10
property int maxGroups: 20 property int maxGroups: 20
Component.onCompleted: { // Priority constants for Android 16-style stacking
groupedNotifications = Qt.createQmlObject(` readonly property int priorityHigh: 2 // Conversations, calls, media
import QtQuick readonly property int priorityNormal: 1 // Regular notifications
ListModel {} readonly property int priorityLow: 0 // System, background updates
`, root)
} // Notification type constants
readonly property int typeConversation: 1
readonly property int typeMedia: 2
readonly property int typeSystem: 3
readonly property int typeNormal: 4
// Format timestamp for display // Format timestamp for display
function formatTimestamp(timestamp) { function formatTimestamp(timestamp) {
@@ -57,6 +76,9 @@ Singleton {
return return
} }
// Enhance notification with priority and type detection
notificationObj = enhanceNotification(notificationObj)
const appName = notificationObj.appName const appName = notificationObj.appName
let groupIndex = appGroupMap[appName] let groupIndex = appGroupMap[appName]
@@ -86,15 +108,36 @@ Singleton {
notificationsList.append(notificationObj) notificationsList.append(notificationObj)
groupedNotifications.append({ // Create properly structured latestNotification object
const latestNotificationData = {
"id": notificationObj.id || "",
"appName": notificationObj.appName || "",
"appIcon": notificationObj.appIcon || "",
"summary": notificationObj.summary || "",
"body": notificationObj.body || "",
"timestamp": notificationObj.timestamp || new Date(),
"priority": notificationObj.priority || priorityNormal,
"notificationType": notificationObj.notificationType || typeNormal,
"urgency": notificationObj.urgency || 1,
"image": notificationObj.image || ""
}
const groupData = {
"appName": appName, "appName": appName,
"appIcon": notificationObj.appIcon || "", "appIcon": notificationObj.appIcon || "",
"notifications": notificationsList, "notifications": notificationsList,
"totalCount": 1, "totalCount": 1,
"latestNotification": notificationObj, "latestNotification": latestNotificationData,
"expanded": false, "expanded": false,
"timestamp": notificationObj.timestamp "timestamp": notificationObj.timestamp || new Date(),
}) "priority": notificationObj.priority || priorityNormal,
"notificationType": notificationObj.notificationType || typeNormal
}
groupedNotifications.append(groupData)
// Sort groups by priority after adding
requestSort()
appGroupMap[appName] = groupIndex appGroupMap[appName] = groupIndex
updateGroupMap() updateGroupMap()
@@ -115,34 +158,83 @@ Singleton {
// Add to front of group (newest first) // Add to front of group (newest first)
group.notifications.insert(0, notificationObj) group.notifications.insert(0, notificationObj)
// Create a new object with proper property structure for latestNotification
const latestNotificationData = {
"id": notificationObj.id || "",
"appName": notificationObj.appName || "",
"appIcon": notificationObj.appIcon || "",
"summary": notificationObj.summary || "",
"body": notificationObj.body || "",
"timestamp": notificationObj.timestamp || new Date(),
"priority": notificationObj.priority || priorityNormal,
"notificationType": notificationObj.notificationType || typeNormal,
"urgency": notificationObj.urgency || 1,
"image": notificationObj.image || ""
}
// Update group metadata // Update group metadata
groupedNotifications.setProperty(groupIndex, "totalCount", group.totalCount + 1) groupedNotifications.setProperty(groupIndex, "totalCount", group.totalCount + 1)
groupedNotifications.setProperty(groupIndex, "latestNotification", notificationObj) groupedNotifications.setProperty(groupIndex, "latestNotification", latestNotificationData)
groupedNotifications.setProperty(groupIndex, "timestamp", notificationObj.timestamp) groupedNotifications.setProperty(groupIndex, "timestamp", notificationObj.timestamp || new Date())
// Update group priority if this notification has higher priority
const currentPriority = group.priority || priorityNormal
const newPriority = Math.max(currentPriority, notificationObj.priority || priorityNormal)
groupedNotifications.setProperty(groupIndex, "priority", newPriority)
// Update notification type if needed
if (notificationObj.notificationType === typeConversation ||
notificationObj.notificationType === typeMedia) {
groupedNotifications.setProperty(groupIndex, "notificationType", notificationObj.notificationType)
}
// Keep only max notifications per group // Keep only max notifications per group
while (group.notifications.count > maxNotificationsPerGroup) { while (group.notifications.count > maxNotificationsPerGroup) {
group.notifications.remove(group.notifications.count - 1) group.notifications.remove(group.notifications.count - 1)
} }
// Move group to front (most recent activity) // Re-sort groups by priority after updating
moveGroupToFront(groupIndex) requestSort()
} }
// Move a group to the front of the list // Request a debounced sort
function moveGroupToFront(groupIndex) { function requestSort() {
if (groupIndex === 0) return // Already at front _sortDirty = true
sortTimer.restart()
const group = groupedNotifications.get(groupIndex) }
if (!group) return
// Sort groups by priority and recency
// Remove from current position function sortGroupsByPriority() {
groupedNotifications.remove(groupIndex) if (groupedNotifications.count <= 1) return
// Insert at front for (let i = 0; i < groupedNotifications.count - 1; i++) {
groupedNotifications.insert(0, group) for (let j = 0; j < groupedNotifications.count - i - 1; j++) {
const groupA = groupedNotifications.get(j)
// Update group map const groupB = groupedNotifications.get(j + 1)
const priorityA = groupA.priority || priorityNormal
const priorityB = groupB.priority || priorityNormal
let shouldSwap = false
if (priorityA !== priorityB) {
if (priorityB > priorityA) {
shouldSwap = true
}
} else {
const timeA = new Date(groupA.timestamp || 0).getTime()
const timeB = new Date(groupB.timestamp || 0).getTime()
if (timeB > timeA) {
shouldSwap = true
}
}
if (shouldSwap) {
// Swap the elements at j and j + 1
groupedNotifications.move(j, j + 1, 1)
}
}
}
updateGroupMap() updateGroupMap()
} }
@@ -200,7 +292,26 @@ Singleton {
// Update latest notification if we removed the latest one // Update latest notification if we removed the latest one
if (notificationIndex === 0 && group.notifications.count > 0) { if (notificationIndex === 0 && group.notifications.count > 0) {
const newLatest = group.notifications.get(0) const newLatest = group.notifications.get(0)
groupedNotifications.setProperty(groupIndex, "latestNotification", newLatest)
// Create a new object with the correct structure
const latestNotificationData = {
"id": newLatest.id || "",
"appName": newLatest.appName || "",
"appIcon": newLatest.appIcon || "",
"summary": newLatest.summary || "",
"body": newLatest.body || "",
"timestamp": newLatest.timestamp || new Date(),
"priority": newLatest.priority || priorityNormal,
"notificationType": newLatest.notificationType || typeNormal,
"urgency": newLatest.urgency || 1,
"image": newLatest.image || ""
}
groupedNotifications.setProperty(groupIndex, "latestNotification", latestNotificationData)
// Update group priority after removal
const newPriority = getGroupPriority(groupIndex)
groupedNotifications.setProperty(groupIndex, "priority", newPriority)
} }
} }
@@ -210,12 +321,12 @@ Singleton {
// Remove an entire group // Remove an entire group
function removeGroup(groupIndex) { function removeGroup(groupIndex) {
if (groupIndex >= groupedNotifications.count) return if (groupIndex >= groupedNotifications.count) return
const group = groupedNotifications.get(groupIndex) const group = groupedNotifications.get(groupIndex)
if (group) { if (group) {
delete appGroupMap[group.appName] delete appGroupMap[group.appName]
groupedNotifications.remove(groupIndex) groupedNotifications.remove(groupIndex)
updateGroupMap() updateGroupMap() // Re-map all group indices
updateTotalCount() updateTotalCount()
} }
} }
@@ -239,6 +350,131 @@ Singleton {
totalCount = count totalCount = count
} }
// Enhance notification with priority and type detection
function enhanceNotification(notificationObj) {
const enhanced = Object.assign({}, notificationObj)
// Detect notification type and priority
enhanced.notificationType = detectNotificationType(enhanced)
enhanced.priority = detectPriority(enhanced)
return enhanced
}
// Detect notification type based on content and app
function detectNotificationType(notification) {
const appName = notification.appName?.toLowerCase() || ""
const summary = notification.summary?.toLowerCase() || ""
const body = notification.body?.toLowerCase() || ""
// Media notifications
if (appName.includes("music") || appName.includes("player") ||
appName.includes("spotify") || appName.includes("youtube") ||
summary.includes("now playing") || summary.includes("playing")) {
return typeMedia
}
// Conversation notifications
if (appName.includes("message") || appName.includes("chat") ||
appName.includes("telegram") || appName.includes("whatsapp") ||
appName.includes("discord") || appName.includes("slack") ||
summary.includes("message") || body.includes("message")) {
return typeConversation
}
// System notifications
if (appName.includes("system") || appName.includes("update") ||
summary.includes("update") || summary.includes("system")) {
return typeSystem
}
return typeNormal
}
// Detect priority based on type and urgency
function detectPriority(notification) {
const notificationType = notification.notificationType
const urgency = notification.urgency || 1 // Default to normal
// High priority for conversations and media
if (notificationType === typeConversation || notificationType === typeMedia) {
return priorityHigh
}
// Low priority for system notifications
if (notificationType === typeSystem) {
return priorityLow
}
// Use urgency for regular notifications
if (urgency >= 2) {
return priorityHigh
} else if (urgency >= 1) {
return priorityNormal
}
return priorityLow
}
// Get group priority (highest priority notification in group)
function getGroupPriority(groupIndex) {
if (groupIndex >= groupedNotifications.count) return priorityLow
const group = groupedNotifications.get(groupIndex)
if (!group) return priorityLow
let maxPriority = priorityLow
for (let i = 0; i < group.notifications.count; i++) {
const notification = group.notifications.get(i)
if (notification && notification.priority > maxPriority) {
maxPriority = notification.priority
}
}
return maxPriority
}
// Generate smart group summary for collapsed state
function generateGroupSummary(group) {
if (!group || !group.notifications || group.notifications.count === 0) {
return ""
}
const notificationCount = group.notifications.count
const latestNotification = group.notifications.get(0)
if (notificationCount === 1) {
return latestNotification.summary || latestNotification.body || ""
}
// For conversations, show sender names
if (latestNotification.notificationType === typeConversation) {
const senders = []
for (let i = 0; i < Math.min(3, notificationCount); i++) {
const notif = group.notifications.get(i)
if (notif && notif.summary && !senders.includes(notif.summary)) {
senders.push(notif.summary)
}
}
if (senders.length > 0) {
const remaining = notificationCount - senders.length
if (remaining > 0) {
return `${senders.join(", ")} and ${remaining} other${remaining > 1 ? "s" : ""}`
}
return senders.join(", ")
}
}
// For media, show current track info
if (latestNotification.notificationType === typeMedia) {
return latestNotification.summary || "Media playing"
}
// Generic summary for other types
return `${notificationCount} notification${notificationCount > 1 ? "s" : ""}`
}
// Get notification by ID across all groups // Get notification by ID across all groups
function getNotificationById(notificationId) { function getNotificationById(notificationId) {
for (let i = 0; i < groupedNotifications.count; i++) { for (let i = 0; i < groupedNotifications.count; i++) {

View File

@@ -0,0 +1,441 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../Common"
import "../Services"
import "../Widgets"
// Demo component to test the enhanced Android 16-style notification system
ApplicationWindow {
id: demoWindow
width: 800
height: 600
visible: true
title: "Android 16 Notification System Demo"
color: Theme.background
Component.onCompleted: {
// Add some sample notifications to demonstrate the system
addSampleNotifications()
}
function addSampleNotifications() {
// High priority conversation notifications
NotificationGroupingService.addNotification({
id: "msg1",
appName: "Messages",
appIcon: "message",
summary: "John Doe",
body: "Hey, are you free for lunch today?",
timestamp: new Date(),
urgency: 2
})
NotificationGroupingService.addNotification({
id: "msg2",
appName: "Messages",
appIcon: "message",
summary: "Jane Smith",
body: "Meeting moved to 3 PM",
timestamp: new Date(Date.now() - 300000), // 5 minutes ago
urgency: 2
})
NotificationGroupingService.addNotification({
id: "msg3",
appName: "Messages",
appIcon: "message",
summary: "John Doe",
body: "Let me know!",
timestamp: new Date(Date.now() - 60000), // 1 minute ago
urgency: 2
})
// Media notification
NotificationGroupingService.addNotification({
id: "media1",
appName: "Spotify",
appIcon: "music_note",
summary: "Now Playing: Gemini Dreams",
body: "Artist: Synthwave Collective",
timestamp: new Date(Date.now() - 120000), // 2 minutes ago
urgency: 1
})
// Regular notifications
NotificationGroupingService.addNotification({
id: "gmail1",
appName: "Gmail",
appIcon: "mail",
summary: "New email from Sarah",
body: "Project update - please review",
timestamp: new Date(Date.now() - 600000), // 10 minutes ago
urgency: 1
})
NotificationGroupingService.addNotification({
id: "gmail2",
appName: "Gmail",
appIcon: "mail",
summary: "Weekly newsletter",
body: "Your weekly digest is ready",
timestamp: new Date(Date.now() - 900000), // 15 minutes ago
urgency: 0
})
// System notifications (low priority)
NotificationGroupingService.addNotification({
id: "sys1",
appName: "System",
appIcon: "settings",
summary: "Software update available",
body: "Update to version 1.2.3",
timestamp: new Date(Date.now() - 1800000), // 30 minutes ago
urgency: 0
})
// Discord conversation
NotificationGroupingService.addNotification({
id: "discord1",
appName: "Discord",
appIcon: "chat",
summary: "Alice in #general",
body: "Anyone up for a game tonight?",
timestamp: new Date(Date.now() - 180000), // 3 minutes ago
urgency: 1
})
NotificationGroupingService.addNotification({
id: "discord2",
appName: "Discord",
appIcon: "chat",
summary: "Bob in #general",
body: "I'm in! What time?",
timestamp: new Date(Date.now() - 150000), // 2.5 minutes ago
urgency: 1
})
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
// Header
Text {
text: "Android 16 Notification System Demo"
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Bold
Layout.fillWidth: true
}
// Stats row
Row {
spacing: Theme.spacingL
Layout.fillWidth: true
Text {
text: "Total Notifications: " + NotificationGroupingService.totalCount
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Groups: " + NotificationGroupingService.groupedNotifications.count
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Button {
text: "Add Sample Notification"
onClicked: addRandomNotification()
anchors.verticalCenter: parent.verticalCenter
}
Button {
text: "Clear All"
onClicked: NotificationGroupingService.clearAllNotifications()
anchors.verticalCenter: parent.verticalCenter
}
}
// Main notification list
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ListView {
id: notificationList
model: NotificationGroupingService.groupedNotifications
spacing: Theme.spacingM
delegate: Column {
width: notificationList.width
spacing: Theme.spacingXS
property var groupData: model
property bool isExpanded: model.expanded || false
// Group header (similar to NotificationHistoryPopup but for demo)
Rectangle {
width: parent.width
height: getPriorityHeight()
radius: Theme.cornerRadius
color: getGroupColor()
// Priority indicator
Rectangle {
width: 4
height: parent.height - 8
anchors.left: parent.left
anchors.leftMargin: 2
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: Theme.primary
visible: (model.priority || 1) === NotificationGroupingService.priorityHigh
}
function getPriorityHeight() {
return (model.priority || 1) === NotificationGroupingService.priorityHigh ? 70 : 60
}
function getGroupColor() {
if ((model.priority || 1) === NotificationGroupingService.priorityHigh) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
}
return 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: 40
height: 40
radius: 20
color: Theme.primaryContainer
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: getTypeIcon()
font.family: Theme.iconFont
font.pixelSize: 20
color: Theme.primaryText
function getTypeIcon() {
const type = model.notificationType || NotificationGroupingService.typeNormal
if (type === NotificationGroupingService.typeConversation) {
return "chat"
} else if (type === NotificationGroupingService.typeMedia) {
return "music_note"
} else if (type === NotificationGroupingService.typeSystem) {
return "settings"
}
return "apps"
}
}
}
// Content
Column {
width: parent.width - 40 - Theme.spacingM - 60
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Row {
spacing: Theme.spacingS
Text {
text: model.appName || "App"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
width: Math.max(countText.width + 6, 18)
height: 18
radius: 9
color: Theme.primary
visible: model.totalCount > 1
anchors.verticalCenter: parent.verticalCenter
Text {
id: countText
anchors.centerIn: parent
text: model.totalCount.toString()
font.pixelSize: 10
color: Theme.primaryText
font.weight: Font.Medium
}
}
Text {
text: getPriorityText()
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
visible: text.length > 0
anchors.verticalCenter: parent.verticalCenter
function getPriorityText() {
const priority = model.priority || NotificationGroupingService.priorityNormal
if (priority === NotificationGroupingService.priorityHigh) {
return "HIGH"
} else if (priority === NotificationGroupingService.priorityLow) {
return "LOW"
}
return ""
}
}
}
Text {
text: NotificationGroupingService.generateGroupSummary(model)
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
}
}
// Expand button
Rectangle {
width: 32
height: 32
radius: 16
color: expandArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: model.totalCount > 1
Text {
anchors.centerIn: parent
text: isExpanded ? "expand_less" : "expand_more"
font.family: Theme.iconFont
font.pixelSize: 18
color: expandArea.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: expandArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NotificationGroupingService.toggleGroupExpansion(index)
}
}
}
}
}
// Expanded notifications
Item {
width: parent.width
height: isExpanded ? expandedContent.height : 0
clip: true
Behavior on height {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Column {
id: expandedContent
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: groupData.notifications
delegate: Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Rectangle {
width: 32
height: 32
radius: 16
color: Theme.primaryContainer
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "notifications"
font.family: Theme.iconFont
font.pixelSize: 16
color: Theme.primaryText
}
}
Column {
width: parent.width - 32 - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
text: model.summary || ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
Text {
text: model.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
}
}
}
}
}
}
}
}
}
}
}
function addRandomNotification() {
const apps = ["Messages", "Gmail", "Discord", "Spotify", "System"]
const summaries = ["New message", "Update available", "Someone mentioned you", "Now playing", "Task completed"]
const bodies = ["This is a sample notification body", "Please check this out", "Important update", "Don't miss this", "Action required"]
const randomApp = apps[Math.floor(Math.random() * apps.length)]
const randomSummary = summaries[Math.floor(Math.random() * summaries.length)]
const randomBody = bodies[Math.floor(Math.random() * bodies.length)]
NotificationGroupingService.addNotification({
id: "random_" + Date.now(),
appName: randomApp,
appIcon: randomApp.toLowerCase(),
summary: randomSummary,
body: randomBody,
timestamp: new Date(),
urgency: Math.floor(Math.random() * 3)
})
}
}

38
Tests/run_notification_demo.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Test script to run the Android 16 notification system demo
echo "Starting Android 16 Notification System Demo..."
echo "This demo showcases the enhanced notification grouping and stacking features."
echo ""
# Check if quickshell is available
if ! command -v quickshell &> /dev/null; then
echo "Error: quickshell is not installed or not in PATH"
echo "Please install quickshell to run this demo"
exit 1
fi
# Navigate to the quickshell config directory
cd "$(dirname "$0")/.." || exit 1
# Run the demo in the background
echo "Running demo with quickshell in the background..."
quickshell -p Tests/NotificationSystemDemo.qml &
QUICKSHELL_PID=$!
# Wait for a few seconds to see if it crashes
sleep 5
# Check if the process is still running
if ps -p $QUICKSHELL_PID > /dev/null; then
echo "Demo is running successfully in the background (PID: $QUICKSHELL_PID)."
echo "Please close the demo window manually to stop the process."
# Kill the process for the purpose of this test
kill $QUICKSHELL_PID
else
echo "Error: The demo crashed or failed to start."
exit 1
fi
echo "Demo test completed."

View File

@@ -0,0 +1,405 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import "../Common"
import "../Services"
// Compact notification group component for Android 16-style collapsed groups
Rectangle {
id: root
property var groupData
property bool isHovered: false
property bool showExpandButton: groupData ? groupData.totalCount > 1 : false
property int groupPriority: groupData ? (groupData.priority || NotificationGroupingService.priorityNormal) : NotificationGroupingService.priorityNormal
property int notificationType: groupData ? (groupData.notificationType || NotificationGroupingService.typeNormal) : NotificationGroupingService.typeNormal
signal expandRequested()
signal groupClicked()
signal groupDismissed()
width: parent.width
height: getCompactHeight()
radius: Theme.cornerRadius
color: getBackgroundColor()
// Enhanced elevation effect for high priority
layer.enabled: groupPriority === NotificationGroupingService.priorityHigh
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 1
shadowBlur: 0.2
shadowColor: Qt.rgba(0, 0, 0, 0.08)
}
function getCompactHeight() {
if (notificationType === NotificationGroupingService.typeMedia) {
return 72 // Slightly taller for media controls
}
return groupPriority === NotificationGroupingService.priorityHigh ? 64 : 56
}
function getBackgroundColor() {
if (isHovered) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
}
if (groupPriority === NotificationGroupingService.priorityHigh) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.04)
}
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.06)
}
// Priority indicator strip
Rectangle {
width: 3
height: parent.height - 8
anchors.left: parent.left
anchors.leftMargin: 2
anchors.verticalCenter: parent.verticalCenter
radius: 1.5
color: getPriorityColor()
visible: groupPriority === NotificationGroupingService.priorityHigh
}
function getPriorityColor() {
if (notificationType === NotificationGroupingService.typeConversation) {
return Theme.primary
} else if (notificationType === NotificationGroupingService.typeMedia) {
return "#FF6B35" // Orange for media
}
return Theme.primary
}
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.leftMargin: groupPriority === NotificationGroupingService.priorityHigh ? Theme.spacingM + 6 : Theme.spacingM
spacing: Theme.spacingM
// App Icon
Rectangle {
width: getIconSize()
height: width
radius: width / 2
color: getIconBackgroundColor()
anchors.verticalCenter: parent.verticalCenter
// Subtle glow for high priority
layer.enabled: groupPriority === NotificationGroupingService.priorityHigh
layer.effect: MultiEffect {
shadowEnabled: true
shadowBlur: 0.4
shadowColor: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
}
function getIconSize() {
if (groupPriority === NotificationGroupingService.priorityHigh) {
return 40
}
return 32
}
function getIconBackgroundColor() {
if (notificationType === NotificationGroupingService.typeConversation) {
return Theme.primaryContainer
} else if (notificationType === NotificationGroupingService.typeMedia) {
return Qt.rgba(1, 0.42, 0.21, 0.2) // Orange tint for media
}
return Theme.primaryContainer
}
// App icon or fallback
Loader {
anchors.fill: parent
sourceComponent: groupData && groupData.appIcon ? iconComponent : fallbackComponent
}
Component {
id: iconComponent
IconImage {
width: parent.width * 0.7
height: width
anchors.centerIn: parent
asynchronous: true
source: {
if (!groupData || !groupData.appIcon) return ""
if (groupData.appIcon.startsWith("file://") || groupData.appIcon.startsWith("/")) {
return groupData.appIcon
}
return Quickshell.iconPath(groupData.appIcon, "image-missing")
}
}
}
Component {
id: fallbackComponent
Text {
anchors.centerIn: parent
text: getDefaultIcon()
font.family: Theme.iconFont
font.pixelSize: parent.width * 0.5
color: Theme.primaryText
function getDefaultIcon() {
if (notificationType === NotificationGroupingService.typeConversation) {
return "chat"
} else if (notificationType === NotificationGroupingService.typeMedia) {
return "music_note"
} else if (notificationType === NotificationGroupingService.typeSystem) {
return "settings"
}
return "apps"
}
}
}
}
// Content area
Column {
width: parent.width - parent.spacing - 40 - (showExpandButton ? 40 : 0) - (notificationType === NotificationGroupingService.typeMedia ? 100 : 0)
anchors.verticalCenter: parent.verticalCenter
spacing: 2
// App name and count
Row {
width: parent.width
spacing: Theme.spacingS
Text {
text: groupData ? groupData.appName : "App"
font.pixelSize: groupPriority === NotificationGroupingService.priorityHigh ? Theme.fontSizeLarge : Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: groupPriority === NotificationGroupingService.priorityHigh ? Font.DemiBold : Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
// Count badge
Rectangle {
width: Math.max(countText.width + 6, 18)
height: 18
radius: 9
color: Theme.primary
visible: groupData && groupData.totalCount > 1
anchors.verticalCenter: parent.verticalCenter
Text {
id: countText
anchors.centerIn: parent
text: groupData ? groupData.totalCount.toString() : "0"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primaryText
font.weight: Font.Medium
}
}
// Time indicator
Text {
text: getTimeText()
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
function getTimeText() {
if (!groupData || !groupData.latestNotification) return ""
return NotificationGroupingService.formatTimestamp(groupData.latestNotification.timestamp)
}
}
}
// Summary text
Text {
text: getSummaryText()
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
visible: text.length > 0
function getSummaryText() {
if (!groupData) return ""
if (groupData.totalCount === 1) {
const notif = groupData.latestNotification
return notif ? (notif.summary || notif.body || "") : ""
}
// Use smart summary for multiple notifications
return NotificationGroupingService.generateGroupSummary(groupData)
}
}
}
// Media controls (if applicable)
Loader {
active: notificationType === NotificationGroupingService.typeMedia
width: active ? 100 : 0
height: parent.height
anchors.verticalCenter: parent.verticalCenter
sourceComponent: Row {
spacing: Theme.spacingS
anchors.centerIn: parent
Rectangle {
width: 28
height: 28
radius: 14
color: Theme.primaryContainer
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: Theme.iconFont
font.pixelSize: 16
color: Theme.primaryText
}
MouseArea {
anchors.fill: parent
onClicked: {
// Handle previous track
console.log("Previous track clicked")
}
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: Theme.primary
Text {
anchors.centerIn: parent
text: "pause" // Could be "play_arrow" based on state
font.family: Theme.iconFont
font.pixelSize: 16
color: Theme.primaryText
}
MouseArea {
anchors.fill: parent
onClicked: {
// Handle play/pause
console.log("Play/pause clicked")
}
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: Theme.primaryContainer
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: Theme.iconFont
font.pixelSize: 16
color: Theme.primaryText
}
MouseArea {
anchors.fill: parent
onClicked: {
// Handle next track
console.log("Next track clicked")
}
}
}
}
}
// Expand button
Rectangle {
width: showExpandButton ? 32 : 0
height: 32
radius: 16
color: expandArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: showExpandButton
Text {
anchors.centerIn: parent
text: "expand_more"
font.family: Theme.iconFont
font.pixelSize: 18
color: expandArea.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: expandArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
expandRequested()
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
// Main interaction area
MouseArea {
anchors.fill: parent
anchors.rightMargin: showExpandButton ? 40 : 0
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
isHovered = true
}
onExited: {
isHovered = false
}
onClicked: {
if (showExpandButton) {
expandRequested()
} else {
groupClicked()
}
}
}
// Swipe gesture for dismissal
DragHandler {
target: null
acceptedDevices: PointerDevice.TouchScreen | PointerDevice.Mouse
property real startX: 0
property real threshold: 100
onActiveChanged: {
if (active) {
startX = centroid.position.x
} else {
const deltaX = centroid.position.x - startX
if (deltaX < -threshold) {
groupDismissed()
}
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -48,7 +48,7 @@ PanelWindow {
color: Theme.popupBackground() color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: 0.5
// TopBar dropdown animation - slide down from bar (consistent with other TopBar widgets) // TopBar dropdown animation - slide down from bar (consistent with other TopBar widgets)
transform: [ transform: [
@@ -225,46 +225,128 @@ PanelWindow {
property var groupData: model property var groupData: model
property bool isExpanded: model.expanded || false property bool isExpanded: model.expanded || false
property int groupPriority: model.priority || NotificationGroupingService.priorityNormal
property int notificationType: model.notificationType || NotificationGroupingService.typeNormal
// Group Header // Group Header with enhanced visual hierarchy
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 56 height: getGroupHeaderHeight()
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: groupHeaderArea.containsMouse ? color: getGroupHeaderColor()
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)
// App Icon // Enhanced elevation effect based on priority
layer.enabled: groupPriority === NotificationGroupingService.priorityHigh
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.4
shadowColor: Qt.rgba(0, 0, 0, 0.1)
}
// Priority indicator strip
Rectangle { Rectangle {
width: 32 width: 4
height: 32 height: parent.height
radius: width / 2
color: Theme.primaryContainer
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM radius: 2
color: getPriorityColor()
visible: groupPriority === NotificationGroupingService.priorityHigh
}
function getGroupHeaderHeight() {
// Dynamic height based on content length and priority
// Calculate height based on message content length
const bodyText = (model.latestNotification && model.latestNotification.body) ? model.latestNotification.body : ""
const bodyLines = Math.min(Math.ceil((bodyText.length / 50)), 4) // Estimate lines needed
const bodyHeight = bodyLines * 16 // 16px per line
const indicatorHeight = model.totalCount > 1 ? 16 : 0
const paddingTop = Theme.spacingM
const paddingBottom = Theme.spacingS
let calculatedHeight = paddingTop + 20 + bodyHeight + indicatorHeight + paddingBottom
// Minimum height based on priority
const minHeight = groupPriority === NotificationGroupingService.priorityHigh ? 90 : 80
return Math.max(calculatedHeight, minHeight)
}
function getGroupHeaderColor() {
if (groupHeaderArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
}
// Different background colors based on priority
if (groupPriority === NotificationGroupingService.priorityHigh) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.05)
}
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
}
function getPriorityColor() {
if (notificationType === NotificationGroupingService.typeConversation) {
return Theme.primary
} else if (notificationType === NotificationGroupingService.typeMedia) {
return "#FF6B35" // Orange for media
}
return Theme.primary
}
// App Icon with enhanced styling
Rectangle {
width: groupPriority === NotificationGroupingService.priorityHigh ? 40 : 32
height: width
radius: width / 2
color: getIconBackgroundColor()
anchors.left: parent.left
anchors.leftMargin: groupPriority === NotificationGroupingService.priorityHigh ? Theme.spacingM + 4 : Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// Material icon fallback // Removed glow effect as requested
function getIconBackgroundColor() {
if (notificationType === NotificationGroupingService.typeConversation) {
return Theme.primaryContainer
} else if (notificationType === NotificationGroupingService.typeMedia) {
return Qt.rgba(1, 0.42, 0.21, 0.2) // Orange tint for media
}
return Theme.primaryContainer
}
// Material icon fallback with type-specific icons
Loader { Loader {
active: !model.appIcon || model.appIcon === "" active: !model.appIcon || model.appIcon === ""
anchors.fill: parent anchors.fill: parent
sourceComponent: Text { sourceComponent: Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "apps" text: getDefaultIcon()
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: 16 font.pixelSize: groupPriority === NotificationGroupingService.priorityHigh ? 20 : 16
color: Theme.primaryText color: Theme.primaryText
function getDefaultIcon() {
if (notificationType === NotificationGroupingService.typeConversation) {
return "chat"
} else if (notificationType === NotificationGroupingService.typeMedia) {
return "music_note"
} else if (notificationType === NotificationGroupingService.typeSystem) {
return "settings"
}
return "apps"
}
} }
} }
// App icon // App icon with priority-based sizing
Loader { Loader {
active: model.appIcon && model.appIcon !== "" active: model.appIcon && model.appIcon !== ""
anchors.centerIn: parent anchors.centerIn: parent
sourceComponent: IconImage { sourceComponent: IconImage {
width: 24 width: groupPriority === NotificationGroupingService.priorityHigh ? 28 : 24
height: 24 height: width
asynchronous: true asynchronous: true
source: { source: {
if (!model.appIcon) return "" if (!model.appIcon) return ""
@@ -277,14 +359,14 @@ PanelWindow {
} }
} }
// App Name and Summary // App Name and Summary with enhanced layout
Column { Column {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM + 32 + Theme.spacingM // Icon + spacing anchors.leftMargin: Theme.spacingM + (groupPriority === NotificationGroupingService.priorityHigh ? 48 : 40) + Theme.spacingM
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 80 // Space for buttons anchors.rightMargin: 32 // Maximum available width for message content
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter // Center the entire content vertically
spacing: 2 spacing: groupPriority === NotificationGroupingService.priorityHigh ? 4 : 2
Row { Row {
width: parent.width width: parent.width
@@ -292,20 +374,29 @@ PanelWindow {
Text { Text {
text: model.appName || "App" text: model.appName || "App"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: groupPriority === NotificationGroupingService.priorityHigh ? Theme.fontSizeLarge : Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: groupPriority === NotificationGroupingService.priorityHigh ? Font.DemiBold : Font.Medium
} }
// Notification count badge // Enhanced notification count badge
Rectangle { Rectangle {
width: Math.max(countText.width + 8, 20) width: Math.max(countText.width + 8, 20)
height: 20 height: 20
radius: 10 radius: 10
color: Theme.primary color: getBadgeColor()
visible: model.totalCount > 1 visible: model.totalCount > 1
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// Removed glow effect as requested
function getBadgeColor() {
if (groupPriority === NotificationGroupingService.priorityHigh) {
return Theme.primary
}
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
}
Text { Text {
id: countText id: countText
anchors.centerIn: parent anchors.centerIn: parent
@@ -317,29 +408,67 @@ PanelWindow {
} }
} }
// Latest message summary (title)
Text { Text {
text: model.latestNotification ? text: getLatestMessageTitle()
(model.latestNotification.summary || model.latestNotification.body || "") : "" font.pixelSize: groupPriority === NotificationGroupingService.priorityHigh ? Theme.fontSizeMedium : Theme.fontSizeSmall
font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceText
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
visible: text.length > 0 visible: text.length > 0
font.weight: Font.Medium
function getLatestMessageTitle() {
if (model.latestNotification) {
return model.latestNotification.summary || ""
}
return ""
}
} }
// Latest message body (content)
Text {
text: getLatestMessageBody()
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
wrapMode: Text.WordWrap
elide: Text.ElideRight
visible: text.length > 0
maximumLineCount: groupPriority === NotificationGroupingService.priorityHigh ? 3 : 2
function getLatestMessageBody() {
if (model.latestNotification) {
return model.latestNotification.body || ""
}
return ""
}
}
// Additional messages indicator removed - moved below as floating text
} }
// Expand/Collapse Icon // Enhanced Expand/Collapse Icon - moved up more for better spacing
Rectangle { Rectangle {
id: expandCollapseButton id: expandCollapseButton
width: 32 width: model.totalCount > 1 ? 32 : 0
height: 32 height: 32
radius: 16 radius: 16
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 40 // More space from close button anchors.rightMargin: 6 // Reduced right margin to add left padding
anchors.verticalCenter: parent.verticalCenter anchors.bottom: parent.bottom
anchors.bottomMargin: 16 // Moved up even more for better spacing
color: expandButtonArea.containsMouse ? color: expandButtonArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
"transparent" "transparent"
visible: model.totalCount > 1
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
@@ -348,11 +477,8 @@ PanelWindow {
font.pixelSize: 20 font.pixelSize: 20
color: expandButtonArea.containsMouse ? Theme.primary : Theme.surfaceText color: expandButtonArea.containsMouse ? Theme.primary : Theme.surfaceText
Behavior on rotation { Behavior on text {
NumberAnimation { enabled: false // Disable animation on text change to prevent flicker
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
} }
} }
@@ -361,6 +487,7 @@ PanelWindow {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: model.totalCount > 1
onClicked: { onClicked: {
NotificationGroupingService.toggleGroupExpansion(index) NotificationGroupingService.toggleGroupExpansion(index)
@@ -416,14 +543,16 @@ PanelWindow {
MouseArea { MouseArea {
id: groupHeaderArea id: groupHeaderArea
anchors.fill: parent anchors.fill: parent
anchors.rightMargin: 76 // Exclude both expand and close button areas anchors.rightMargin: 32 // Adjusted for maximum content width
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: model.totalCount > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
preventStealing: false preventStealing: false
propagateComposedEvents: true propagateComposedEvents: true
onClicked: { onClicked: {
NotificationGroupingService.toggleGroupExpansion(index) if (model.totalCount > 1) {
NotificationGroupingService.toggleGroupExpansion(index)
}
} }
} }
@@ -435,16 +564,75 @@ PanelWindow {
} }
} }
// Expanded Notifications List // Floating "More messages" indicator - positioned below the main group
Rectangle {
width: Math.min(parent.width * 0.8, 200)
height: 24
radius: 12
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 1
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
border.width: 1
visible: model.totalCount > 1 && !isExpanded
// Smooth fade animation
opacity: (model.totalCount > 1 && !isExpanded) ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Text {
anchors.centerIn: parent
text: getFloatingIndicatorText()
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.9)
font.weight: Font.Medium
function getFloatingIndicatorText() {
if (model.totalCount > 1) {
const additionalCount = model.totalCount - 1
return `${additionalCount} more message${additionalCount > 1 ? "s" : ""} Tap to expand`
}
return ""
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NotificationGroupingService.toggleGroupExpansion(index)
}
}
// Subtle hover effect
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Expanded Notifications List with enhanced animation
Item { Item {
width: parent.width width: parent.width
height: isExpanded ? expandedContent.height : 0 height: isExpanded ? expandedContent.height + Theme.spacingS : 0
clip: true clip: true
// Enhanced staggered animation
Behavior on height { Behavior on height {
NumberAnimation { SequentialAnimation {
duration: Theme.mediumDuration NumberAnimation {
easing.type: Theme.emphasizedEasing duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
} }
} }
@@ -453,10 +641,13 @@ PanelWindow {
width: parent.width width: parent.width
spacing: Theme.spacingXS spacing: Theme.spacingXS
opacity: isExpanded ? 1.0 : 0.0 opacity: isExpanded ? 1.0 : 0.0
topPadding: Theme.spacingS
bottomPadding: Theme.spacingM
// Enhanced opacity animation
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.mediumDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
@@ -465,6 +656,8 @@ PanelWindow {
model: groupData.notifications model: groupData.notifications
delegate: Rectangle { delegate: Rectangle {
// Skip the first (latest) notification since it's shown in the header
visible: index > 0
width: parent.width width: parent.width
height: 80 height: 80
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -472,6 +665,37 @@ PanelWindow {
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : 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) Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
// Subtle left border for nested notifications
Rectangle {
width: 2
height: parent.height - 16
anchors.left: parent.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
radius: 1
}
// Smooth appearance animation
opacity: isExpanded ? 1.0 : 0.0
transform: Translate {
y: isExpanded ? 0 : -10
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on transform {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
// Individual notification close button // Individual notification close button
Rectangle { Rectangle {
width: 24 width: 24
@@ -508,6 +732,7 @@ PanelWindow {
Row { Row {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
anchors.leftMargin: Theme.spacingM + 8 // Extra space for border
anchors.rightMargin: 36 anchors.rightMargin: 36
spacing: Theme.spacingM spacing: Theme.spacingM

View File

@@ -242,7 +242,7 @@ PanelWindow {
} }
} }
// Small dismiss button - bottom right corner // Small dismiss button - bottom right corner with better positioning
Rectangle { Rectangle {
width: 60 width: 60
height: 18 height: 18
@@ -257,7 +257,7 @@ PanelWindow {
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.rightMargin: 12 anchors.rightMargin: 12
anchors.bottomMargin: 10 anchors.bottomMargin: 14 // Moved up for better padding
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
@@ -312,7 +312,7 @@ PanelWindow {
anchors.fill: parent anchors.fill: parent
anchors.margins: 12 anchors.margins: 12
anchors.rightMargin: 32 anchors.rightMargin: 32
anchors.bottomMargin: 6 // Reduced bottom margin to account for dismiss button anchors.bottomMargin: 24 // Even more space to ensure 2px+ margin above dismiss button
spacing: 12 spacing: 12
// Notification icon based on EXAMPLE NotificationAppIcon pattern // Notification icon based on EXAMPLE NotificationAppIcon pattern
@@ -426,17 +426,52 @@ PanelWindow {
// Text content // Text content
Column { Column {
width: parent.width - 68 width: parent.width - 68
anchors.verticalCenter: parent.verticalCenter anchors.top: parent.top
spacing: 4 anchors.topMargin: 4 // Move content up slightly
spacing: 3
Text { // Title and timestamp row
text: root.activeNotification ? (root.activeNotification.summary || "") : "" Row {
font.pixelSize: 14
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width width: parent.width
elide: Text.ElideRight spacing: 8
visible: text.length > 0
Text {
text: root.activeNotification ? (root.activeNotification.summary || "") : ""
font.pixelSize: 14
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width - timestampText.width - parent.spacing
elide: Text.ElideRight
visible: text.length > 0
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: timestampText
text: root.activeNotification ? formatNotificationTime(root.activeNotification.timestamp) : ""
font.pixelSize: 9
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: text.length > 0
anchors.verticalCenter: parent.verticalCenter
function formatNotificationTime(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)
if (diffMinutes < 1) {
return "now"
} else if (diffMinutes < 60) {
return `${diffMinutes}m`
} else {
const diffHours = Math.floor(diffMs / 3600000)
return `${diffHours}h`
}
}
}
} }
Text { Text {

View File

@@ -33,15 +33,16 @@ Rectangle {
width: 18 width: 18
height: 18 height: 18
source: { source: {
let icon = trayItem?.icon || ""; let icon = trayItem?.icon;
if (!icon) return ""; if (typeof icon === 'string' || icon instanceof String) {
if (icon.includes("?path=")) {
if (icon.includes("?path=")) { const [name, path] = icon.split("?path=");
const [name, path] = icon.split("?path="); const fileName = name.substring(name.lastIndexOf("/") + 1);
const fileName = name.substring(name.lastIndexOf("/") + 1); return `file://${path}/${fileName}`;
return `file://${path}/${fileName}`; }
return icon;
} }
return icon; return ""; // Return empty string if icon is not a string
} }
asynchronous: true asynchronous: true
smooth: true smooth: true

View File

@@ -2,6 +2,7 @@ TopBar 1.0 TopBar/TopBar.qml
TrayMenuPopup 1.0 TrayMenuPopup.qml TrayMenuPopup 1.0 TrayMenuPopup.qml
NotificationPopup 1.0 NotificationPopup.qml NotificationPopup 1.0 NotificationPopup.qml
NotificationHistoryPopup 1.0 NotificationHistoryPopup.qml NotificationHistoryPopup 1.0 NotificationHistoryPopup.qml
NotificationCompactGroup 1.0 NotificationCompactGroup.qml
WifiPasswordDialog 1.0 WifiPasswordDialog.qml WifiPasswordDialog 1.0 WifiPasswordDialog.qml
AppLauncher 1.0 AppLauncher.qml AppLauncher 1.0 AppLauncher.qml
ClipboardHistory 1.0 ClipboardHistory.qml ClipboardHistory 1.0 ClipboardHistory.qml