Files
z-bar-qt/Modules/NotificationCenter.qml
T
2025-11-09 01:13:05 +01:00

450 lines
19 KiB
QML

import Quickshell
import Quickshell.Hyprland
import Quickshell.Widgets
import Quickshell.Io
import QtQuick.Layouts
import QtQuick.Controls.FluentWinUI3
import QtQuick.Effects
import QtQuick
import Quickshell.Services.Notifications
PanelWindow {
id: root
color: "transparent"
anchors {
top: true
right: true
left: true
bottom: true
}
required property list<Notification> notifications
property bool centerShown: false
property alias posX: backgroundRect.x
property alias doNotDisturb: dndSwitch.checked
visible: false
IpcHandler {
id: ipcHandler
target: "root"
function showCenter(): void { root.centerShown = !root.centerShown; }
}
onVisibleChanged: {
if ( root.visible ) {
showAnimation.start();
}
}
onCenterShownChanged: {
if ( !root.centerShown ) {
closeAnimation.start();
closeTimer.start();
} else {
root.visible = true;
}
}
Timer {
id: closeTimer
interval: 300
onTriggered: {
root.visible = false;
}
}
NumberAnimation {
id: showAnimation
target: backgroundRect
property: "x"
from: Screen.width
to: Screen.width - backgroundRect.implicitWidth - 10
duration: 300
easing.type: Easing.OutCubic
}
NumberAnimation {
id: closeAnimation
target: backgroundRect
property: "x"
from: Screen.width - backgroundRect.implicitWidth - 10
to: Screen.width
duration: 300
easing.type: Easing.OutCubic
}
QtObject {
id: groupedData
property var groups: ({})
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 );
}
// Sort notifications within each group (latest first)
for ( var app in newGroups ) {
newGroups[ app ].sort(( a, b ) => b.receivedTime - a.receivedTime );
}
groups = newGroups;
groupsChanged();
}
Component.onCompleted: updateGroups()
}
Connections {
target: root
function onNotificationsChanged() {
groupedData.updateGroups();
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: {
if ( root.centerShown ) {
root.centerShown = false;
}
}
}
Rectangle {
id: backgroundRect
y: 10
x: Screen.width
implicitWidth: 400
implicitHeight: root.height - 20
color: "#801a1a1a"
radius: 8
border.color: "#555555"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
RowLayout {
Layout.fillWidth: true
Switch {
id: dndSwitch
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 {
color: "#333333"
Layout.preferredHeight: 1
Layout.fillWidth: true
}
Flickable {
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: notificationColumn.implicitHeight
clip: true
Column {
id: notificationColumn
width: parent.width
spacing: 10
add: Transition {
NumberAnimation {
properties: "x";
duration: 300;
easing.type: Easing.OutCubic
}
}
move: Transition {
NumberAnimation {
properties: "y,x";
duration: 200;
easing.type: Easing.OutCubic
}
}
Repeater {
model: Object.keys( groupedData.groups )
Column {
id: groupColumn
required property string modelData
property var notifications: groupedData.groups[ modelData ]
width: parent.width
spacing: 10
property bool shouldShow: false
property bool isExpanded: false
move: Transition {
NumberAnimation {
properties: "y,x";
duration: 100;
easing.type: Easing.OutCubic
}
}
RowLayout {
width: parent.width
height: 30
Text {
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.leftMargin: 5
text: groupColumn.modelData
color: "white"
font.pointSize: 14
font.bold: true
}
Rectangle {
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
Layout.fillHeight: true
Layout.preferredWidth: 30
color: collapseArea.containsMouse ? "#15FFFFFF" : "transparent"
radius: 4
visible: groupColumn.isExpanded
Text {
anchors.centerIn: parent
text: "\ue944"
font.family: "Material Symbols Rounded"
font.pointSize: 18
color: "white"
}
MouseArea {
id: collapseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
groupColumn.isExpanded = false;
}
}
}
}
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
// 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;
// }
// }
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(); });
}
}
}
}
}
}
}
}
}
}
}
}