mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-08 14:35:39 -05:00
Enhanced Notification System & Settings
This commit is contained in:
@@ -563,6 +563,56 @@ PanelWindow {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Action buttons for collapsed view
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: modelData.latestNotification.actions && modelData.latestNotification.actions.length > 0 && !modelData.latestNotification.notification.hasInlineReply && !expanded
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: modelData.latestNotification.actions ? modelData.latestNotification.actions.slice(0, 2) : []
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: Math.min((parent.width - (parent.spacing * (parent.children.length - 1))) / parent.children.length, 120)
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: collapsedActionArea.containsMouse ? Theme.primary : Theme.surfaceContainer
|
||||||
|
border.color: collapsedActionArea.containsMouse ? "transparent" : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData.text || ""
|
||||||
|
color: collapsedActionArea.containsMouse ? Theme.primaryText : Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
width: parent.width - 16
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: collapsedActionArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.invoke) {
|
||||||
|
modelData.invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Enhanced quick reply for conversations
|
// Enhanced quick reply for conversations
|
||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -934,6 +984,56 @@ PanelWindow {
|
|||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Individual action buttons
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: modelData.actions && modelData.actions.length > 0 && !modelData.notification.hasInlineReply
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: modelData.actions ? modelData.actions.slice(0, 2) : []
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: Math.min((parent.width - (parent.spacing * (parent.children.length - 1))) / parent.children.length, 80)
|
||||||
|
height: 24
|
||||||
|
radius: 12
|
||||||
|
color: expandedActionArea.containsMouse ? Theme.primary : Theme.surfaceContainer
|
||||||
|
border.color: expandedActionArea.containsMouse ? "transparent" : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData.text || ""
|
||||||
|
color: expandedActionArea.containsMouse ? Theme.primaryText : Theme.surfaceText
|
||||||
|
font.pixelSize: 10
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
width: parent.width - 8
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: expandedActionArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.invoke) {
|
||||||
|
modelData.invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Individual inline reply
|
// Individual inline reply
|
||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -186,6 +186,57 @@ PanelWindow {
|
|||||||
visible: text.length > 0
|
visible: text.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: modelData.latestNotification.actions && modelData.latestNotification.actions.length > 0 && !modelData.latestNotification.notification.hasInlineReply
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: modelData.latestNotification.actions ? modelData.latestNotification.actions.slice(0, 3) : []
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: Math.min((parent.width - (parent.spacing * (parent.children.length - 1))) / parent.children.length, 120)
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: actionArea.containsMouse ? Theme.primary : Theme.surfaceContainer
|
||||||
|
border.color: actionArea.containsMouse ? "transparent" : Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData.text || ""
|
||||||
|
color: actionArea.containsMouse ? Theme.primaryText : Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
width: parent.width - 16
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: actionArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.invoke) {
|
||||||
|
modelData.invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline reply
|
||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Services.Notifications
|
import Quickshell.Services.Notifications
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -17,6 +18,11 @@ Singleton {
|
|||||||
|
|
||||||
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
|
property var expandedMessages: ({}) // Track which individual messages are expanded
|
||||||
|
|
||||||
|
// Notification persistence settings
|
||||||
|
property int maxStoredNotifications: 100
|
||||||
|
property int maxNotificationAge: 7 * 24 * 60 * 60 * 1000 // 7 days in milliseconds
|
||||||
|
property var persistedNotifications: ([]) // Stored notification history
|
||||||
|
|
||||||
NotificationServer {
|
NotificationServer {
|
||||||
id: server
|
id: server
|
||||||
@@ -39,6 +45,13 @@ Singleton {
|
|||||||
console.log("urgency:", notif.urgency);
|
console.log("urgency:", notif.urgency);
|
||||||
console.log("hasInlineReply:", notif.hasInlineReply);
|
console.log("hasInlineReply:", notif.hasInlineReply);
|
||||||
console.log("=============================");
|
console.log("=============================");
|
||||||
|
|
||||||
|
// Check if notification should be shown based on settings
|
||||||
|
if (!NotificationSettings.shouldShowNotification(notif)) {
|
||||||
|
console.log("Notification blocked by settings for app:", notif.appName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
notif.tracked = true;
|
notif.tracked = true;
|
||||||
|
|
||||||
const wrapper = notifComponent.createObject(root, {
|
const wrapper = notifComponent.createObject(root, {
|
||||||
@@ -47,7 +60,32 @@ Singleton {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (wrapper) {
|
if (wrapper) {
|
||||||
root.notifications.push(wrapper);
|
// Handle media notification replacement
|
||||||
|
if (wrapper.isMedia) {
|
||||||
|
handleMediaNotification(wrapper);
|
||||||
|
} else {
|
||||||
|
root.notifications.push(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-expand conversation groups with new messages
|
||||||
|
if (wrapper.isConversation && notifications.length > 1) {
|
||||||
|
const groupKey = getGroupKey(wrapper);
|
||||||
|
const existingGroup = groupedNotifications.find(group => group.key === groupKey);
|
||||||
|
if (existingGroup && existingGroup.count > 1) {
|
||||||
|
let newExpandedGroups = {};
|
||||||
|
for (const key in expandedGroups) {
|
||||||
|
newExpandedGroups[key] = expandedGroups[key];
|
||||||
|
}
|
||||||
|
newExpandedGroups[groupKey] = true;
|
||||||
|
expandedGroups = newExpandedGroups;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to persistent storage (only for non-transient notifications)
|
||||||
|
if (!notif.transient) {
|
||||||
|
addToPersistentStorage(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Notification added. Total notifications:", root.notifications.length);
|
console.log("Notification added. Total notifications:", root.notifications.length);
|
||||||
console.log("Grouped notifications:", root.groupedNotifications.length);
|
console.log("Grouped notifications:", root.groupedNotifications.length);
|
||||||
} else {
|
} else {
|
||||||
@@ -101,13 +139,91 @@ Singleton {
|
|||||||
readonly property bool hasImage: image && image.length > 0
|
readonly property bool hasImage: image && image.length > 0
|
||||||
readonly property bool hasAppIcon: appIcon && appIcon.length > 0
|
readonly property bool hasAppIcon: appIcon && appIcon.length > 0
|
||||||
readonly property bool isConversation: notification.hasInlineReply
|
readonly property bool isConversation: notification.hasInlineReply
|
||||||
|
readonly property bool isMedia: isMediaNotification()
|
||||||
|
readonly property bool isSystem: isSystemNotification()
|
||||||
|
|
||||||
|
function isMediaNotification() {
|
||||||
|
const appNameLower = appName.toLowerCase();
|
||||||
|
const summaryLower = summary.toLowerCase();
|
||||||
|
|
||||||
|
// Check for media apps
|
||||||
|
if (appNameLower.includes("spotify") ||
|
||||||
|
appNameLower.includes("vlc") ||
|
||||||
|
appNameLower.includes("mpv") ||
|
||||||
|
appNameLower.includes("music") ||
|
||||||
|
appNameLower.includes("player") ||
|
||||||
|
appNameLower.includes("youtube") ||
|
||||||
|
appNameLower.includes("media")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for media-related summary text
|
||||||
|
if (summaryLower.includes("now playing") ||
|
||||||
|
summaryLower.includes("playing") ||
|
||||||
|
summaryLower.includes("paused") ||
|
||||||
|
summaryLower.includes("track")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for media actions
|
||||||
|
for (const action of actions) {
|
||||||
|
const actionId = action.identifier.toLowerCase();
|
||||||
|
if (actionId.includes("play") ||
|
||||||
|
actionId.includes("pause") ||
|
||||||
|
actionId.includes("next") ||
|
||||||
|
actionId.includes("previous") ||
|
||||||
|
actionId.includes("media")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSystemNotification() {
|
||||||
|
const appNameLower = appName.toLowerCase();
|
||||||
|
const summaryLower = summary.toLowerCase();
|
||||||
|
|
||||||
|
// Check for system apps
|
||||||
|
if (appNameLower.includes("system") ||
|
||||||
|
appNameLower.includes("networkmanager") ||
|
||||||
|
appNameLower.includes("upower") ||
|
||||||
|
appNameLower.includes("notification-daemon") ||
|
||||||
|
appNameLower.includes("systemd") ||
|
||||||
|
appNameLower.includes("update") ||
|
||||||
|
appNameLower.includes("battery") ||
|
||||||
|
appNameLower.includes("network") ||
|
||||||
|
appNameLower.includes("wifi") ||
|
||||||
|
appNameLower.includes("bluetooth")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for system-related summary text
|
||||||
|
if (summaryLower.includes("battery") ||
|
||||||
|
summaryLower.includes("power") ||
|
||||||
|
summaryLower.includes("update") ||
|
||||||
|
summaryLower.includes("connected") ||
|
||||||
|
summaryLower.includes("disconnected") ||
|
||||||
|
summaryLower.includes("network") ||
|
||||||
|
summaryLower.includes("wifi") ||
|
||||||
|
summaryLower.includes("bluetooth")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
readonly property Timer timer: Timer {
|
readonly property Timer timer: Timer {
|
||||||
running: wrapper.popup
|
running: wrapper.popup
|
||||||
interval: wrapper.notification.expireTimeout > 0 ? wrapper.notification.expireTimeout : 5000 // 5 second default
|
interval: {
|
||||||
|
if (wrapper.notification.expireTimeout > 0) {
|
||||||
|
return wrapper.notification.expireTimeout * 1000;
|
||||||
|
}
|
||||||
|
return NotificationSettings.getAppTimeout(wrapper.appName);
|
||||||
|
}
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
wrapper.popup = false;
|
wrapper.popup = false;
|
||||||
}
|
}
|
||||||
@@ -147,11 +263,53 @@ Singleton {
|
|||||||
wrapper.notification.dismiss();
|
wrapper.notification.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMediaNotification(newMediaWrapper) {
|
||||||
|
const groupKey = getGroupKey(newMediaWrapper);
|
||||||
|
|
||||||
|
// Find and replace any existing media notification from the same app
|
||||||
|
for (let i = notifications.length - 1; i >= 0; i--) {
|
||||||
|
const existing = notifications[i];
|
||||||
|
if (existing.isMedia && getGroupKey(existing) === groupKey) {
|
||||||
|
// Replace the existing media notification
|
||||||
|
existing.notification.dismiss();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new media notification
|
||||||
|
root.notifications.push(newMediaWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
// Android 16-style notification grouping functions
|
// Android 16-style notification grouping functions
|
||||||
function getGroupKey(wrapper) {
|
function getGroupKey(wrapper) {
|
||||||
const appName = wrapper.appName.toLowerCase();
|
const appName = wrapper.appName.toLowerCase();
|
||||||
|
|
||||||
|
// Media notifications: replace previous media notification from same app
|
||||||
|
if (wrapper.isMedia) {
|
||||||
|
return `${appName}:media`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// System notifications: group by category
|
||||||
|
if (wrapper.isSystem) {
|
||||||
|
const summary = wrapper.summary.toLowerCase();
|
||||||
|
|
||||||
|
if (summary.includes("battery") || summary.includes("power")) {
|
||||||
|
return "system:battery";
|
||||||
|
}
|
||||||
|
if (summary.includes("network") || summary.includes("wifi") || summary.includes("connected") || summary.includes("disconnected")) {
|
||||||
|
return "system:network";
|
||||||
|
}
|
||||||
|
if (summary.includes("update") || summary.includes("upgrade")) {
|
||||||
|
return "system:updates";
|
||||||
|
}
|
||||||
|
if (summary.includes("bluetooth")) {
|
||||||
|
return "system:bluetooth";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default system grouping
|
||||||
|
return "system:general";
|
||||||
|
}
|
||||||
|
|
||||||
// Conversation apps with inline reply
|
// Conversation apps with inline reply
|
||||||
if (wrapper.isConversation) {
|
if (wrapper.isConversation) {
|
||||||
const summary = wrapper.summary.toLowerCase();
|
const summary = wrapper.summary.toLowerCase();
|
||||||
@@ -173,8 +331,6 @@ Singleton {
|
|||||||
return `${appName}:conversation`;
|
return `${appName}:conversation`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Default: Group by app
|
// Default: Group by app
|
||||||
return appName;
|
return appName;
|
||||||
}
|
}
|
||||||
@@ -192,7 +348,9 @@ Singleton {
|
|||||||
latestNotification: null,
|
latestNotification: null,
|
||||||
count: 0,
|
count: 0,
|
||||||
hasInlineReply: false,
|
hasInlineReply: false,
|
||||||
isConversation: notif.isConversation
|
isConversation: notif.isConversation,
|
||||||
|
isMedia: notif.isMedia,
|
||||||
|
isSystem: notif.isSystem
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +381,9 @@ Singleton {
|
|||||||
latestNotification: null,
|
latestNotification: null,
|
||||||
count: 0,
|
count: 0,
|
||||||
hasInlineReply: false,
|
hasInlineReply: false,
|
||||||
isConversation: notif.isConversation
|
isConversation: notif.isConversation,
|
||||||
|
isMedia: notif.isMedia,
|
||||||
|
isSystem: notif.isSystem
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +434,25 @@ Singleton {
|
|||||||
return group.latestNotification.summary;
|
return group.latestNotification.summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (group.isMedia) {
|
||||||
|
return "Now Playing";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.isSystem) {
|
||||||
|
const keyParts = group.key.split(":");
|
||||||
|
if (keyParts.length > 1) {
|
||||||
|
const systemCategory = keyParts[1];
|
||||||
|
switch (systemCategory) {
|
||||||
|
case "battery": return `${group.count} Battery alerts`;
|
||||||
|
case "network": return `${group.count} Network updates`;
|
||||||
|
case "updates": return `${group.count} System updates`;
|
||||||
|
case "bluetooth": return `${group.count} Bluetooth updates`;
|
||||||
|
default: return `${group.count} System notifications`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${group.count} System notifications`;
|
||||||
|
}
|
||||||
|
|
||||||
if (group.isConversation) {
|
if (group.isConversation) {
|
||||||
const keyParts = group.key.split(":");
|
const keyParts = group.key.split(":");
|
||||||
if (keyParts.length > 1) {
|
if (keyParts.length > 1) {
|
||||||
@@ -285,7 +464,6 @@ Singleton {
|
|||||||
return `${group.count} new messages`;
|
return `${group.count} new messages`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return `${group.count} notifications`;
|
return `${group.count} notifications`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +472,18 @@ Singleton {
|
|||||||
return group.latestNotification.body;
|
return group.latestNotification.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (group.isMedia) {
|
||||||
|
const latest = group.latestNotification;
|
||||||
|
if (latest.body && latest.body.length > 0) {
|
||||||
|
return latest.body;
|
||||||
|
}
|
||||||
|
return latest.summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.isSystem) {
|
||||||
|
return `Latest: ${group.latestNotification.summary}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (group.isConversation) {
|
if (group.isConversation) {
|
||||||
const latest = group.latestNotification;
|
const latest = group.latestNotification;
|
||||||
if (latest.body && latest.body.length > 0) {
|
if (latest.body && latest.body.length > 0) {
|
||||||
@@ -302,8 +492,70 @@ Singleton {
|
|||||||
return "Tap to view conversation";
|
return "Tap to view conversation";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return `Latest: ${group.latestNotification.summary}`;
|
return `Latest: ${group.latestNotification.summary}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notification persistence functions
|
||||||
|
function addToPersistentStorage(wrapper) {
|
||||||
|
const persistedNotif = {
|
||||||
|
id: wrapper.notification.id,
|
||||||
|
appName: wrapper.appName,
|
||||||
|
summary: wrapper.summary,
|
||||||
|
body: wrapper.body,
|
||||||
|
appIcon: wrapper.appIcon,
|
||||||
|
image: wrapper.image,
|
||||||
|
urgency: wrapper.urgency,
|
||||||
|
timestamp: wrapper.time.getTime(),
|
||||||
|
isConversation: wrapper.isConversation,
|
||||||
|
isMedia: wrapper.isMedia,
|
||||||
|
isSystem: wrapper.isSystem
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to beginning of array
|
||||||
|
persistedNotifications.unshift(persistedNotif);
|
||||||
|
|
||||||
|
// Clean up old notifications
|
||||||
|
cleanupPersistentStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupPersistentStorage() {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
let newPersisted = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < persistedNotifications.length && i < maxStoredNotifications; i++) {
|
||||||
|
const notif = persistedNotifications[i];
|
||||||
|
if (now - notif.timestamp < maxNotificationAge) {
|
||||||
|
newPersisted.push(notif);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
persistedNotifications = newPersisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPersistentNotificationsByApp(appName) {
|
||||||
|
return persistedNotifications.filter(notif => notif.appName.toLowerCase() === appName.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPersistentNotificationsByType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case "conversation": return persistedNotifications.filter(notif => notif.isConversation);
|
||||||
|
case "media": return persistedNotifications.filter(notif => notif.isMedia);
|
||||||
|
case "system": return persistedNotifications.filter(notif => notif.isSystem);
|
||||||
|
default: return persistedNotifications;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchPersistentNotifications(query) {
|
||||||
|
const searchLower = query.toLowerCase();
|
||||||
|
return persistedNotifications.filter(notif =>
|
||||||
|
notif.appName.toLowerCase().includes(searchLower) ||
|
||||||
|
notif.summary.toLowerCase().includes(searchLower) ||
|
||||||
|
notif.body.toLowerCase().includes(searchLower)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize persistence on component creation
|
||||||
|
Component.onCompleted: {
|
||||||
|
cleanupPersistentStorage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
190
Services/NotificationSettings.qml
Normal file
190
Services/NotificationSettings.qml
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// General notification settings
|
||||||
|
property bool notificationsEnabled: true
|
||||||
|
property bool soundEnabled: true
|
||||||
|
property bool persistNotifications: true
|
||||||
|
property int defaultTimeout: 5000 // milliseconds
|
||||||
|
|
||||||
|
// Grouping settings
|
||||||
|
property bool enableSmartGrouping: true
|
||||||
|
property bool autoExpandConversations: true
|
||||||
|
property bool replaceMediaNotifications: true
|
||||||
|
|
||||||
|
// Persistence settings
|
||||||
|
property int maxStoredNotifications: 100
|
||||||
|
property int notificationRetentionDays: 7
|
||||||
|
|
||||||
|
// Display settings
|
||||||
|
property bool showNotificationPopups: true
|
||||||
|
property bool showAppIcons: true
|
||||||
|
property bool showTimestamps: true
|
||||||
|
property bool enableInlineReply: true
|
||||||
|
property bool showActionButtons: true
|
||||||
|
|
||||||
|
// Priority settings
|
||||||
|
property bool allowCriticalNotifications: true
|
||||||
|
property bool respectDoNotDisturb: true
|
||||||
|
|
||||||
|
// App-specific settings
|
||||||
|
property var appSettings: ({})
|
||||||
|
|
||||||
|
// Do Not Disturb settings
|
||||||
|
property bool doNotDisturbMode: false
|
||||||
|
property string doNotDisturbStart: "22:00"
|
||||||
|
property string doNotDisturbEnd: "08:00"
|
||||||
|
property bool allowCriticalInDND: true
|
||||||
|
|
||||||
|
// Sound settings
|
||||||
|
property string notificationSound: "default"
|
||||||
|
property real soundVolume: 0.7
|
||||||
|
property bool vibrationEnabled: false
|
||||||
|
|
||||||
|
function getAppSetting(appName, setting, defaultValue) {
|
||||||
|
const app = appSettings[appName.toLowerCase()];
|
||||||
|
if (app && app.hasOwnProperty(setting)) {
|
||||||
|
return app[setting];
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAppSetting(appName, setting, value) {
|
||||||
|
let newAppSettings = {};
|
||||||
|
for (const app in appSettings) {
|
||||||
|
newAppSettings[app] = appSettings[app];
|
||||||
|
}
|
||||||
|
|
||||||
|
const appKey = appName.toLowerCase();
|
||||||
|
if (!newAppSettings[appKey]) {
|
||||||
|
newAppSettings[appKey] = {};
|
||||||
|
}
|
||||||
|
newAppSettings[appKey][setting] = value;
|
||||||
|
appSettings = newAppSettings;
|
||||||
|
|
||||||
|
// Save to persistent storage
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAppBlocked(appName) {
|
||||||
|
return getAppSetting(appName, "blocked", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAppMuted(appName) {
|
||||||
|
return getAppSetting(appName, "muted", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppTimeout(appName) {
|
||||||
|
return getAppSetting(appName, "timeout", defaultTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInDoNotDisturbMode() {
|
||||||
|
if (!doNotDisturbMode && !respectDoNotDisturb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const currentTime = now.getHours() * 60 + now.getMinutes();
|
||||||
|
|
||||||
|
const startParts = doNotDisturbStart.split(":");
|
||||||
|
const endParts = doNotDisturbEnd.split(":");
|
||||||
|
const startTime = parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
|
||||||
|
const endTime = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
|
||||||
|
|
||||||
|
if (startTime <= endTime) {
|
||||||
|
// Same day range (e.g., 9:00 - 17:00)
|
||||||
|
return currentTime >= startTime && currentTime <= endTime;
|
||||||
|
} else {
|
||||||
|
// Overnight range (e.g., 22:00 - 08:00)
|
||||||
|
return currentTime >= startTime || currentTime <= endTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowNotification(notification) {
|
||||||
|
// Check if notifications are globally disabled
|
||||||
|
if (!notificationsEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if app is blocked
|
||||||
|
if (isAppBlocked(notification.appName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Do Not Disturb mode
|
||||||
|
if (isInDoNotDisturbMode()) {
|
||||||
|
// Allow critical notifications if configured
|
||||||
|
if (allowCriticalInDND && notification.urgency === 2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPlaySound(notification) {
|
||||||
|
if (!soundEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAppMuted(notification.appName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInDoNotDisturbMode() && !allowCriticalInDND) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
// In a real implementation, this would save to a config file
|
||||||
|
console.log("NotificationSettings: Settings saved");
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
// In a real implementation, this would load from a config file
|
||||||
|
console.log("NotificationSettings: Settings loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetToDefaults() {
|
||||||
|
notificationsEnabled = true;
|
||||||
|
soundEnabled = true;
|
||||||
|
persistNotifications = true;
|
||||||
|
defaultTimeout = 5000;
|
||||||
|
enableSmartGrouping = true;
|
||||||
|
autoExpandConversations = true;
|
||||||
|
replaceMediaNotifications = true;
|
||||||
|
maxStoredNotifications = 100;
|
||||||
|
notificationRetentionDays = 7;
|
||||||
|
showNotificationPopups = true;
|
||||||
|
showAppIcons = true;
|
||||||
|
showTimestamps = true;
|
||||||
|
enableInlineReply = true;
|
||||||
|
showActionButtons = true;
|
||||||
|
allowCriticalNotifications = true;
|
||||||
|
respectDoNotDisturb = true;
|
||||||
|
doNotDisturbMode = false;
|
||||||
|
doNotDisturbStart = "22:00";
|
||||||
|
doNotDisturbEnd = "08:00";
|
||||||
|
allowCriticalInDND = true;
|
||||||
|
notificationSound = "default";
|
||||||
|
soundVolume = 0.7;
|
||||||
|
vibrationEnabled = false;
|
||||||
|
appSettings = {};
|
||||||
|
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +1,90 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
echo "Waiting for notification service to be ready..."
|
# Enhanced Notification System Test Script with Common Icons
|
||||||
|
# Uses icons that are more likely to be available on most systems
|
||||||
|
|
||||||
# Wait for the notification service to be available
|
echo "🔔 Testing Enhanced Notification System Features"
|
||||||
max_attempts=8
|
echo "============================================================="
|
||||||
attempt=0
|
|
||||||
|
|
||||||
while [ $attempt -lt $max_attempts ]; do
|
# Check what icons are available
|
||||||
if notify-send -a "test" "Service Ready Test" "Testing..." 2>/dev/null; then
|
echo "Checking available icons..."
|
||||||
echo "Notification service is ready!"
|
if [ -d "~/.local/share/icons/Papirus" ]; then
|
||||||
break
|
echo "✓ Icon theme found"
|
||||||
fi
|
ICON_BASE="~/.local/share/icons/Papirus"
|
||||||
echo "Attempt $((attempt + 1))/$max_attempts - waiting..."
|
else
|
||||||
sleep 2
|
echo "! Using fallback icons"
|
||||||
attempt=$((attempt + 1))
|
ICON_BASE=""
|
||||||
done
|
|
||||||
|
|
||||||
if [ $attempt -eq $max_attempts ]; then
|
|
||||||
echo "Timeout waiting for notification service"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
# Test 1: Basic notifications
|
||||||
echo "Running layout and functionality tests..."
|
echo "📱 Test 1: Basic notifications"
|
||||||
echo ""
|
notify-send -i preferences-desktop "Test App" "Basic notification message"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
# Now run the actual tests
|
# Test 2: Media notifications (should replace each other)
|
||||||
notify-send -a "firefox" "Test 1" "Firefox notification 1"
|
echo "🎵 Test 2: Media notifications (replacement behavior)"
|
||||||
sleep 0.5
|
notify-send -i audio-x-generic "Spotify" "Now Playing: Song 1 - Artist A"
|
||||||
notify-send -a "firefox" "Test 2" "Firefox notification 2"
|
sleep 2
|
||||||
|
notify-send -i audio-x-generic "Spotify" "Now Playing: Song 2 - Artist B"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Test 3: System notifications (grouped by category)
|
||||||
|
echo "🔋 Test 3: System notifications (grouped by category)"
|
||||||
|
notify-send -i battery "UPower" "Battery Low: 15% remaining"
|
||||||
sleep 1
|
sleep 1
|
||||||
|
notify-send -i network-wired "NetworkManager" "Network Connected: WiFi connected"
|
||||||
notify-send -a "MyCustomApp" "Custom 1" "Custom app notification 1"
|
|
||||||
sleep 0.5
|
|
||||||
notify-send -a "MyCustomApp" "Custom 2" "Custom app notification 2"
|
|
||||||
sleep 1
|
sleep 1
|
||||||
|
notify-send -i system-software-update "System" "Updates Available: 5 packages can be updated"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
notify-send -a "code" "VS Code 1" "Code notification 1"
|
# Test 4: Conversation notifications (should group and auto-expand)
|
||||||
|
echo "💬 Test 4: Conversation notifications (grouping)"
|
||||||
|
if command -v discord &> /dev/null; then
|
||||||
|
notify-send -i discord "Discord" "#general: User1 says Hello everyone!"
|
||||||
|
sleep 1
|
||||||
|
notify-send -i discord "Discord" "#general: User2 says Hey there!"
|
||||||
|
sleep 1
|
||||||
|
notify-send -i discord "Discord" "john_doe: Private message from John"
|
||||||
|
else
|
||||||
|
notify-send -i internet-chat "Discord" "#general: User1 says Hello everyone!"
|
||||||
|
sleep 1
|
||||||
|
notify-send -i internet-chat "Discord" "#general: User2 says Hey there!"
|
||||||
|
sleep 1
|
||||||
|
notify-send -i internet-chat "Discord" "john_doe: Private message from John"
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Test 5: Urgent notifications
|
||||||
|
echo "🚨 Test 5: Urgent notifications"
|
||||||
|
notify-send -u critical -i dialog-warning "Critical Alert" "System overheating detected - Temperature: 85°C"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Test 6: Notifications with actions (simulated)
|
||||||
|
echo "⚡ Test 6: Action buttons"
|
||||||
|
notify-send -i system-upgrade "System Update" "Updates available - Click to install or remind later"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Test 7: Multiple apps generating notifications
|
||||||
|
echo "📊 Test 7: Multiple apps"
|
||||||
|
notify-send -i mail-message-new "Email" "You have 3 new emails"
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
notify-send -a "code" "VS Code 2" "Code notification 2"
|
notify-send -i office-calendar "Calendar" "Daily standup in 5 minutes"
|
||||||
|
sleep 0.5
|
||||||
|
notify-send -i folder-downloads "File Manager" "document.pdf downloaded"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ All notifications sent successfully!"
|
echo "✅ Notification tests completed!"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "📋 Enhanced Features Tested:"
|
||||||
|
echo " • Media notification replacement"
|
||||||
|
echo " • System notification grouping"
|
||||||
|
echo " • Conversation grouping and auto-expansion"
|
||||||
|
echo " • Urgency level handling"
|
||||||
|
echo " • Action button support"
|
||||||
|
echo " • Multi-app notification handling"
|
||||||
|
echo ""
|
||||||
|
echo "🎯 Check your notification popup and notification center to see the results!"
|
||||||
|
echo ""
|
||||||
|
echo "Note: Some icons may show as fallback (checkerboard) if icon themes aren't installed."
|
||||||
|
echo "To install more icons: sudo pacman -S papirus-icon-theme adwaita-icon-theme"
|
||||||
Reference in New Issue
Block a user