notification daemon + center

This commit is contained in:
Zacharias-Brohn
2025-11-09 01:13:05 +01:00
parent 6d52325d89
commit 176b5c79b7
10 changed files with 956 additions and 254 deletions
+280 -251
View File
@@ -1,5 +1,3 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Widgets
@@ -23,7 +21,6 @@ PanelWindow {
property bool centerShown: false
property alias posX: backgroundRect.x
property alias doNotDisturb: dndSwitch.checked
property alias groupedData: groupedData
visible: false
IpcHandler {
@@ -76,94 +73,35 @@ PanelWindow {
easing.type: Easing.OutCubic
}
ListModel {
QtObject {
id: groupedData
property int totalCount: 0
property var groupMap: ({})
property var groups: ({})
function mapGroups() {
groupedData.groupMap = {};
for ( var i = 0; i < groupedData.count; i++ ) {
var name = get(i).name;
groupMap[ name ] = i;
}
}
function updateCount() {
var count = 0;
for ( var i = 0; i < groupedData.count; i++ ) {
count += get(i).notifications.count;
}
totalCount = count;
}
function ensureGroup(appName) {
for ( var i = 0; i < count; i++ ) {
if ( get(i).name === appName ) {
return get(i).notifications;
function updateGroups() {
var newGroups = {};
for ( var i = 0; i < root.notifications.length; i++ ) {
var notif = root.notifications[ i ];
var appName = notif.appName || "Unknown";
if ( !newGroups[ appName ]) {
newGroups[ appName ] = [];
}
newGroups[ appName ].push( notif );
}
var model = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', root);
append({ name: appName, notifications: model });
mapGroups();
return model;
}
function addNotification(notif) {
var appName = notif.appName
var model = ensureGroup(appName);
model.insert(0, notif);
updateCount();
}
function removeNotification(notif) {
var appName = notif.appName || "Unknown";
var group = get( groupMap[ appName ]);
if ( group.name === appName ) {
root.notifications[ idMap[ notif.id ]].dismiss();
if ( group.notifications.count === 0 ) {
remove( groupMap[ appName ], 1 );
mapGroups();
}
// Sort notifications within each group (latest first)
for ( var app in newGroups ) {
newGroups[ app ].sort(( a, b ) => b.receivedTime - a.receivedTime );
}
updateCount();
groups = newGroups;
groupsChanged();
}
function removeGroup(notif) {
var appName = notif.appName || "Unknown";
var group = get(groupMap[ appName ]);
if ( group.name === appName ) {
for ( var i = 0; i < group.notifications.count; i++ ) {
var item = group.notifications.get( i );
item.dismiss();
}
remove( groupMap[ appName ], 1 );
updateCount();
mapGroups();
}
}
function resetGroups(notifications) {
groupedData.clear();
for (var i = 0; i < root.notifications.length; i++) {
addNotification(root.notifications[i]);
}
updateCount();
}
Component.onCompleted: {
resetGroups(root.notifications)
}
Component.onCompleted: updateGroups()
}
Connections {
target: root
function onNotificationsChanged() {
if ( root.notifications.length > groupedData.totalCount ) {
groupedData.addNotification( root.notifications[ root.notifications.length - 1 ] );
groupedData.mapGroups();
console.log(root.notifications)
}
groupedData.updateGroups();
}
}
@@ -173,8 +111,6 @@ PanelWindow {
onClicked: {
if ( root.centerShown ) {
root.centerShown = false;
console.log("groups", groupedData.count);
console.log(root.notifications)
}
}
}
@@ -189,29 +125,51 @@ PanelWindow {
radius: 8
border.color: "#555555"
border.width: 1
clip: true
ColumnLayout {
id: mainLayout
anchors.fill: parent
anchors.margins: 10
spacing: 10
RowLayout {
Layout.preferredHeight: 30
Layout.fillWidth: true
Text {
text: "Notifications"
color: "white"
font.bold: true
font.pointSize: 16
Layout.fillWidth: true
}
Switch {
id: dndSwitch
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true
text: "Do Not Disturb"
}
RowLayout {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Text {
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
text: "Clear all"
color: "white"
}
Rectangle {
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.preferredWidth: 30
Layout.preferredHeight: 30
color: clearArea.containsMouse ? "#15FFFFFF" : "transparent"
radius: 4
Text {
anchors.centerIn: parent
text: "\ue0b8"
font.family: "Material Symbols Rounded"
font.pointSize: 18
color: "white"
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
for ( var app in groupedData.groups ) {
groupedData.groups[ app ].forEach( function( n ) { n.dismiss(); });
}
}
}
}
}
}
Rectangle {
@@ -220,194 +178,265 @@ PanelWindow {
Layout.fillWidth: true
}
Column {
id: notificationColumn
Layout.fillHeight: true
Flickable {
Layout.fillWidth: true
// width: mainLayout.width
spacing: 10
Layout.fillHeight: true
contentHeight: notificationColumn.implicitHeight
clip: true
move: Transition {
NumberAnimation {
properties: "y,x"
duration: 200
easing.type: Easing.InOutQuad
}
}
add: Transition {
NumberAnimation {
properties: "y,x"
duration: 200
easing.type: Easing.InOutQuad
}
}
ListView {
id: groupListView
model: groupedData
Column {
id: notificationColumn
width: parent.width
spacing: 10
width: 400
height: Math.min(contentHeight, notificationColumn.height)
// Layout.fillWidth: true
// Layout.fillHeight: true
delegate: ListView {
required property var modelData
required property int index
property bool isExpanded: false
property bool isExpandedAnim: false
id: listView
visible: true
property ListModel notificationsModel: modelData.notifications
model: notificationsModel
width: notificationColumn.width
implicitHeight: listView.isExpandedAnim ? contentHeight : 80
clip: true
spacing: 10
onIsExpandedChanged: {
if ( !isExpanded ) {
collapseAnim.start();
} else {
expandAnim.start();
}
}
add: Transition {
NumberAnimation {
id: collapseAnim
target: listView
property: "implicitHeight"
to: 80
from: listView.contentHeight
duration: 80
easing.type: Easing.InOutQuad
onStopped: {
listView.isExpandedAnim = listView.isExpanded;
}
properties: "x";
duration: 300;
easing.type: Easing.OutCubic
}
}
move: Transition {
NumberAnimation {
id: expandAnim
target: listView
property: "implicitHeight"
from: 80
to: listView.contentHeight
duration: 80
easing.type: Easing.InOutQuad
onStopped: {
listView.isExpandedAnim = listView.isExpanded;
}
properties: "y,x";
duration: 200;
easing.type: Easing.OutCubic
}
}
Behavior on y {
NumberAnimation {
duration: 200
easing.type: Easing.InOutQuad
}
}
Repeater {
model: Object.keys( groupedData.groups )
Column {
id: groupColumn
required property string modelData
property var notifications: groupedData.groups[ modelData ]
width: parent.width
spacing: 10
displaced: Transition {
NumberAnimation {
properties: "y,x"
duration: 200
easing.type: Easing.InOutQuad
}
}
property bool shouldShow: false
property bool isExpanded: false
delegate: Rectangle {
id: notificationItem
required property var modelData
required property int index
width: listView.width
height: 80
color: "#801a1a1a"
border.color: "#555555"
border.width: 1
radius: 8
clip: true
visible: true
opacity: 1
MouseArea {
anchors.fill: parent
onClicked: {
listView.isExpanded ? ( notificationItem.index === 0 ? listView.isExpanded = false : null ) : listView.isExpanded = true;
move: Transition {
NumberAnimation {
properties: "y,x";
duration: 100;
easing.type: Easing.OutCubic
}
}
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 10
width: parent.width
height: 30
IconImage {
source: notificationItem.modelData.image
Layout.preferredWidth: 48
Layout.preferredHeight: 48
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Text {
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.leftMargin: 5
text: groupColumn.modelData
color: "white"
font.pointSize: 14
font.bold: true
}
ColumnLayout {
Layout.fillWidth: true
Rectangle {
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.fillHeight: true
spacing: 4
Layout.preferredWidth: 30
color: collapseArea.containsMouse ? "#15FFFFFF" : "transparent"
radius: 4
visible: groupColumn.isExpanded
Text {
text: notificationItem.modelData.appName
anchors.centerIn: parent
text: "\ue944"
font.family: "Material Symbols Rounded"
font.pointSize: 18
color: "white"
font.bold: true
font.pointSize: 12
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: notificationItem.modelData.summary
color: "white"
font.pointSize: 10
elide: Text.ElideRight
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
Text {
text: notificationItem.modelData.body
color: "#dddddd"
font.pointSize: 8
elide: Text.ElideRight
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
MouseArea {
id: collapseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
groupColumn.isExpanded = false;
}
}
}
}
Rectangle {
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
width: 18
height: 18
color: closeArea.containsMouse ? "#FF6077" : "transparent"
radius: 9
Repeater {
model: groupColumn.notifications
Rectangle {
id: groupHeader
required property var modelData
required property var index
width: parent.width
height: groupColumn.isExpanded ? ( modelData.actions.length > 1 ? 130 : 80 ) : 80
color: "#801a1a1a"
border.color: "#555555"
border.width: 1
radius: 8
visible: groupColumn.notifications[0].id === modelData.id || groupColumn.isExpanded
Text {
anchors.centerIn: parent
text: "✕"
color: closeArea.containsMouse ? "white" : "#888888"
font.pointSize: 12
}
// NumberAnimation {
// id: expandAnim
// target: groupHeader
// property: "y"
// duration: 300
// easing.type: Easing.OutCubic
// from: (( groupHeader.height / 2 ) * index )
// to: (( groupHeader.height + 60 ) * index )
// onStarted: {
// groupColumn.shouldShow = true;
// }
// }
//
// NumberAnimation {
// id: collapseAnim
// target: groupHeader
// property: "y"
// duration: 300
// easing.type: Easing.OutCubic
// from: (( groupHeader.height + 60 ) * index )
// to: (( groupHeader.height / 2 ) * index )
// onStopped: {
// groupColumn.isExpanded = false;
// }
// }
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.notifications[modelData].dismiss();
Connections {
target: groupColumn
function onShouldShowChanged() {
if ( !shouldShow ) {
// collapseAnim.start();
}
}
}
onVisibleChanged: {
if ( visible ) {
// expandAnim.start();
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if ( groupColumn.isExpanded ) {
if ( groupHeader.modelData.actions.length === 1 ) {
groupHeader.modelData.actions[0].invoke();
}
} else {
groupColumn.isExpanded = true;
}
}
}
Column {
anchors.fill: parent
anchors.margins: 10
RowLayout {
height: 80
width: parent.width
spacing: 10
IconImage {
source: groupHeader.modelData.image
Layout.preferredWidth: 48
Layout.preferredHeight: 48
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.topMargin: 5
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
Text {
text: groupHeader.modelData.summary
color: "white"
font.bold: true
font.pointSize: 12
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: groupHeader.modelData.body
font.pointSize: 10
color: "#dddddd"
elide: Text.ElideRight
Layout.fillWidth: true
Layout.fillHeight: true
}
}
Text {
text: groupColumn.notifications.length > 1 ? ( groupColumn.isExpanded ? "" : "(" + groupColumn.notifications.length + ")" ) : ""
font.pointSize: 10
color: "#666666"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
}
}
RowLayout {
spacing: 2
visible: groupColumn.isExpanded && groupHeader.modelData.actions.length > 1
height: 15
width: parent.width
Repeater {
model: groupHeader.modelData.actions
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 30
required property var modelData
color: buttonArea.containsMouse ? "#15FFFFFF" : "#09FFFFFF"
radius: 4
Text {
anchors.centerIn: parent
text: modelData.text
color: "white"
font.pointSize: 12
}
MouseArea {
id: buttonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
modelData.invoke();
}
}
}
}
}
}
Rectangle {
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
width: 18
height: 18
color: closeArea.containsMouse ? "#FF6077" : "transparent"
radius: 9
Text {
anchors.centerIn: parent
text: "✕"
color: closeArea.containsMouse ? "white" : "#888888"
font.pointSize: 12
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
groupColumn.isExpanded ? groupColumn.notifications[0].dismiss() : groupColumn.notifications.forEach( function( n ) { n.dismiss(); });
}
}
}
}
}
-1
View File
@@ -17,7 +17,6 @@ Scope {
notification.tracked = true;
notification.receivedTime = Date.now();
root.notifIds.push(notification.id);
notificationCenter.groupedData.addNotification(notification);
notificationComponent.createObject(root, { notif: notification, visible: !notificationCenter.doNotDisturb });
}
}