Add notifications and notification center

This commit is contained in:
2026-01-22 13:38:54 -08:00
parent 7e93b4afa1
commit add0b3d66f
5 changed files with 351 additions and 3 deletions

View File

@@ -10,6 +10,6 @@ IpcHandler {
item.open(); item.open();
} }
function toggle() { function toggle() {
item.visible ? close() : open() item.toggle();
} }
} }

View File

@@ -21,6 +21,9 @@ PanelWindow {
visible = true visible = true
list.visible = true list.visible = true
} }
function toggle() {
launcher.visible ? close() : open();
}
implicitWidth: 800 implicitWidth: 800
exclusiveZone: 0 exclusiveZone: 0
@@ -63,7 +66,8 @@ PanelWindow {
launcher.visible = false launcher.visible = false
break; break;
case Qt.Key_Return: case Qt.Key_Return:
list.currentItem.execute() if (list.currentItem) list.currentItem.execute();
break;
} }
} }
@@ -148,7 +152,7 @@ PanelWindow {
position: 0 position: 0
} }
GradientStop { GradientStop {
color: Qt.darker(button.color, 4) color: Qt.darker(button.color, 3)
position: 1 position: 1
} }
} }

176
NotifCenter.qml Normal file
View File

@@ -0,0 +1,176 @@
import Quickshell
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
PanelWindow {
id: root
anchors {
top: true
right: true
bottom: true
}
visible: false
exclusiveZone: 1
implicitWidth: 500
color: "transparent"
function open() {
root.visible = true;
}
function close() {
exitAnim.start();
}
function toggle() {
if (exitAnim.running) return;
if (root.visible) close();
else open();
}
required property var tracked
Rectangle {
color: "#222"
anchors.fill: parent
Text {
id: title
anchors {
left: parent.left
top: parent.top
leftMargin: 12
topMargin: 12
}
text: "Notifications"
font.pixelSize: 20
font.family: "Comfortaa"
font.bold: true
color: "white"
}
Button {
anchors {
top: parent.top
right: parent.right
}
width: clearAllText.width + 32
height: clearAllText.height + 30
Text {
id: clearAllText
anchors {
right: parent.right
top: parent.top
bottom: parent.bottom
topMargin: 14
rightMargin: 16
}
color: "white"
font.pixelSize: 16
font.family: "Comfortaa"
text: "Clear All"
}
onClicked: {while (root.tracked.values.length) {
root.tracked.values[0].tracked = false;
}}
property var hover: HoverHandler { }
background: Rectangle {
anchors {
fill: parent
topMargin: 6
leftMargin: 8
bottomMargin: 6
rightMargin: 8
}
radius: 12
color: parent.hover.hovered ? "#333" : "#222"
Behavior on color { PropertyAnimation {
duration: 150
}}
}
}
ListView {
anchors {
fill: parent
topMargin: 48
}
model: root.tracked
spacing: 10
delegate: Button {
anchors {
left: parent.left
right: parent.right
margins: 12
}
height: notifContent.childrenRect.height + 32
Image {
id: notifIcon
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 12
}
width: 40
height: 40
source: Quickshell.iconPath(modelData.appIcon, true)
}
Column {
id: notifContent
anchors {
left: parent.left
right: parent.right
top: parent.top
topMargin: 16
leftMargin: 16 + (notifIcon.source == "" ? 0 : 44)
rightMargin: 16
}
spacing: 6
Text {
anchors {
left: parent.left
right: parent.right
}
color: "white"
font.family: "Comfortaa"
font.pixelSize: 16
text: modelData.summary
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Text {
anchors {
left: parent.left
right: parent.right
}
color: "white"
font.family: "Comfortaa"
font.pixelSize: 12
text: modelData.body
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 4
}
}
background: Rectangle {
color: "#333"
radius: 12
}
onClicked: modelData.tracked = false
}
}
transform: Translate {
NumberAnimation on x {
running: root.visible
from: root.implicitWidth
to: 0
duration: 300
easing.type: Easing.OutQuint
}
NumberAnimation on x {
id: exitAnim
to: root.implicitWidth
duration: 300
easing.type: Easing.InQuint
onFinished: root.visible = false
}
}
}
}

152
Notifs.qml Normal file
View File

@@ -0,0 +1,152 @@
import Quickshell
import Quickshell.Services.Notifications
import QtQuick
import QtQuick.Effects
PanelWindow {
id: root
visible: false
anchors {
top: true
right: true
}
color: "transparent"
aboveWindows: true
implicitWidth: 500
implicitHeight: 200
focusable: false
property var font: { family: "Comfortaa" }
Component {
id: notifPopup
Rectangle {
id: notifRect
required property var modelData
y: {
let res = 0;
for (let i = 0; notifsList.children[i] && notifsList.children[i] != this; i++) {
if (!notifsList.children[i].exiting) res += notifsList.children[i].height + 10;
}
return res;
}
Behavior on y { NumberAnimation {
duration: 300
easing.type: Easing.InOutCubic
}}
property bool exiting: false
width: parent?.width ?? 0
height: notifContent.childrenRect.height + 32
radius: 20
color: "#181818"
RectangularShadow {
z: -1
anchors.fill: parent
blur: 12
radius: parent.radius
}
Image {
id: notifIcon
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 12
}
width: 40
height: 40
source: Quickshell.iconPath(modelData.appIcon, true)
}
Column {
id: notifContent
anchors {
left: parent.left
right: parent.right
top: parent.top
topMargin: 16
leftMargin: 16 + (notifIcon.source == "" ? 0 : 44)
rightMargin: 16
}
spacing: 6
Text {
anchors {
left: parent.left
right: parent.right
}
color: "white"
font.family: "Comfortaa"
font.pixelSize: 16
text: modelData.summary
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Text {
anchors {
left: parent.left
right: parent.right
}
color: "white"
font.family: "Comfortaa"
font.pixelSize: 12
text: modelData.body
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 4
}
}
NumberAnimation on x {
running: root.visible
from: root.width
to: 0
duration: 300
easing.type: Easing.OutBack
easing.overshoot: 0.8
}
NumberAnimation on x {
id: exitAnim
running: false
to: parent.parent.width
duration: 300
easing.type: Easing.InCubic
onStarted: exiting = true
onFinished: {
if (notifsList.children.length === 1) {
root.visible = false
root.implicitHeight = 200;
}
notifRect.destroy();
}
}
Timer {
running: true
repeat: false
interval: 5000
onTriggered: exitAnim.start()
}
Component.onCompleted: {
let sumHeight = 40;
for (const notif of notifsList.children) {
sumHeight += notif.height + 6;
}
root.implicitHeight = Math.max(root.implicitHeight, sumHeight);
}
}
}
Item {
id: notifsList
anchors {
fill: parent
margins: 12
leftMargin: 64
}
}
function notify(notif) {
if (notif.lastGeneration) return
root.visible = true;
notifPopup.createObject(notifsList, {modelData: notif});
}
}

View File

@@ -1,9 +1,18 @@
import "." as Osu import "." as Osu
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Notifications
ShellRoot { ShellRoot {
id: root id: root
NotificationServer {
id: notificationServer
onNotification: (notif) => {
notif.tracked = true
notifPopup.notify(notif)
}
}
readonly property var font: { readonly property var font: {
family: "comfortaa" family: "comfortaa"
} }
@@ -15,4 +24,11 @@ ShellRoot {
target: "lock" target: "lock"
item: Osu.Lock { } item: Osu.Lock { }
} }
Osu.IpcToggle {
target: "notifications"
item: Osu.NotifCenter {
tracked: notificationServer.trackedNotifications
}
}
Osu.Notifs { id: notifPopup }
} }