dots/notifications/NotificationCenter.qml
2025-06-13 13:31:22 -04:00

278 lines
12 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import "../widgets" as Widgets
import ".."
Singleton {
PersistentProperties {
id: persist
property bool notificationsOpen: false
}
IpcHandler {
id: ipc
target: "notifications"
function open(): void {
persist.notificationsOpen = true;
}
function close(): void {
persist.notificationsOpen = false;
}
function toggle(): void {
persist.notificationsOpen = !persist.notificationsOpen;
}
}
LazyLoader {
id: loader
activeAsync: persist.notificationsOpen
PanelWindow {
id: notificationPanel
color: "red"
implicitWidth: 500
exclusionMode: ExclusionMode.Ignore
// WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
anchors {
top: true
right: true
bottom: true
}
ColumnLayout {
spacing: 10
anchors {
fill: parent
margins: 10
}
Text {
text: "Notifications: " + toastList.count
Layout.fillWidth: true
}
ListView {
id: toastList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 5
model: ScriptModel {
values: {
const notifications = Notifications.notificationServer.trackedNotifications.values.concat();
const groupedByApp = notifications.reduce((groups, notification) => {
const appName = notification.appName;
if (!groups[appName]) {
groups[appName] = {
appName: appName,
summaryGroups: {}
};
}
const summary = notification.summary;
const image = notification.image;
if (!groups[appName].summaryGroups[summary]) {
groups[appName].summaryGroups[summary] = {
summary: summary,
image: image,
notifications: []
};
}
groups[appName].summaryGroups[summary].notifications.push(notification);
return groups;
}, {});
return Object.values(groupedByApp).map(appGroup => {
return {
appName: appGroup.appName,
summaryGroups: Object.values(appGroup.summaryGroups)
};
});
}
}
delegate: Item {
id: toastWrapper
required property var modelData
width: ListView.view.width
height: toastContent.height
Item {
id: toastContent
width: parent.width
height: contentColumn.implicitHeight
anchors.centerIn: parent
ColumnLayout {
id: contentColumn
spacing: 2
anchors {
fill: parent
margins: 0
}
// Notification content
Repeater {
model: toastWrapper.modelData.summaryGroups
delegate: Rectangle {
id: summaryGroup
required property var modelData
required property int index
Layout.fillWidth: true
Layout.preferredHeight: groupContent.implicitHeight + 24
color: ShellSettings.colors["surface_container"]
antialiasing: true
topLeftRadius: index === 0 ? 25 : 5
topRightRadius: index === 0 ? 25 : 5
bottomLeftRadius: index === (toastWrapper.modelData.summaryGroups.length - 1) ? 25 : 5
bottomRightRadius: index === (toastWrapper.modelData.summaryGroups.length - 1) ? 25 : 5
ColumnLayout {
id: groupContent
spacing: 8
anchors {
fill: parent
margins: 12
}
RowLayout {
spacing: 12
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
ColumnLayout {
Layout.alignment: Qt.AlignTop
spacing: 0
Item {
id: imageContainer
Layout.preferredWidth: 36
Layout.preferredHeight: 36
visible: summaryGroup.modelData.image != ""
antialiasing: true
Image {
id: notificationImage
anchors.fill: parent
source: summaryGroup.modelData.image
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: notificationImage.width
height: notificationImage.height
radius: notificationImage.width / 2
antialiasing: true
}
}
}
}
}
// Content column
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: 8
// Header row
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: summaryGroup.modelData.summary
font.pixelSize: 16
font.weight: Font.Medium
color: ShellSettings.colors["on_surface"]
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
}
Widgets.Separator {}
Text {
text: "now"
font.pixelSize: 14
color: ShellSettings.colors["on_surface_variant"]
Layout.alignment: Qt.AlignVCenter
}
}
// Notification bodies
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Repeater {
model: summaryGroup.modelData.notifications
delegate: ColumnLayout {
id: bodyDelegate
required property var modelData
required property int index
Layout.fillWidth: true
spacing: 0
Text {
Layout.fillWidth: true
text: bodyDelegate.modelData.body
font.pixelSize: 14
color: ShellSettings.colors["on_surface_variant"]
wrapMode: Text.WordWrap
maximumLineCount: 4
elide: Text.ElideRight
lineHeight: 1.3
visible: bodyDelegate.modelData.body != ""
}
}
}
}
}
}
}
}
}
}
}
}
}
}
// HyprlanFocusGrab {
// id: grab
// windows: [notificationPanel]
// onCleared: {
// ipc.hide();
// }
// }
}
}
function init() {
}
}