feat: basic notifications

Also fix pixel issue with notif area background
Add more log rules to run script
This commit is contained in:
2 * r + 2 * t 2025-05-10 17:31:04 +10:00
parent ed55afbcd8
commit 6b64c8a918
9 changed files with 313 additions and 24 deletions

15
config/NotifsConfig.qml Normal file
View file

@ -0,0 +1,15 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
readonly property int defaultExpireTimeout: 3000
readonly property Sizes sizes: Sizes {}
component Sizes: QtObject {
readonly property int width: 400
readonly property int image: 50
readonly property int badge: 20
}
}

View file

@ -1,5 +1,6 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/config"
import Quickshell
import Quickshell.Widgets
@ -53,18 +54,9 @@ MouseArea {
active: BarConfig.tray.recolourIcons
asynchronous: true
sourceComponent: MultiEffect {
sourceComponent: Colouriser {
source: icon
colorizationColor: root.colour
colorization: 1
Behavior on colorizationColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
}

View file

@ -49,6 +49,10 @@ Shape {
radiusX: root.rounding
radiusY: root.rounding
}
PathLine {
relativeX: 1
relativeY: 0
}
Behavior on fillColor {
ColorAnimation {

View file

@ -7,16 +7,18 @@ import QtQuick
Column {
id: root
// padding: Appearance.padding.large
padding: Appearance.padding.large
anchors.bottom: parent.bottom
anchors.right: parent.right
spacing: Appearance.spacing.normal
StyledRect {
width: 300
height: 100
// color: Qt.rgba(255, 0, 0, 0.4)
Repeater {
model: ScriptModel {
values: [...Notifs.list]
}
Notification {}
}
}

View file

@ -0,0 +1,188 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Effects
StyledRect {
id: root
required property Notifs.Notif modelData
readonly property bool hasImage: modelData.image.length > 0
readonly property bool hasAppIcon: modelData.appIcon.length > 0
readonly property int imageSize: summary.height + bodyPreview.height
property bool expanded
color: Colours.palette.m3surfaceContainer
radius: Appearance.rounding.normal
implicitWidth: NotifsConfig.sizes.width
Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Appearance.padding.normal
implicitHeight: summary.height + bodyPreview.height + anchors.margins * 2
Loader {
id: image
active: root.hasImage
asynchronous: true
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -parent.anchors.margins
width: root.hasImage || root.hasAppIcon ? root.imageSize : 0
height: root.hasImage || root.hasAppIcon ? root.imageSize : 0
visible: root.hasImage || root.hasAppIcon
sourceComponent: ClippingRectangle {
radius: Appearance.rounding.full
width: root.imageSize
height: root.imageSize
Image {
anchors.fill: parent
source: Qt.resolvedUrl(root.modelData.image)
fillMode: Image.PreserveAspectCrop
cache: false
asynchronous: true
}
}
states: State {
name: "expanded"
when: root.expanded
AnchorChanges {
anchors.verticalCenter: undefined
anchors.top: image.parent.top
}
}
}
Loader {
id: appIcon
active: root.hasAppIcon
asynchronous: true
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
anchors.right: root.hasImage ? image.right : undefined
anchors.bottom: root.hasImage ? image.bottom : undefined
sourceComponent: StyledRect {
radius: Appearance.rounding.full
color: Colours.palette.m3tertiaryContainer
implicitWidth: root.hasImage ? NotifsConfig.sizes.badge : root.imageSize
implicitHeight: root.hasImage ? NotifsConfig.sizes.badge : root.imageSize
IconImage {
id: icon
anchors.centerIn: parent
visible: false
implicitSize: Math.round(parent.width * 0.6)
source: Quickshell.iconPath(root.modelData.appIcon)
asynchronous: true
}
Colouriser {
anchors.fill: icon
source: icon
colorizationColor: Colours.palette.m3onTertiaryContainer
}
}
states: State {
name: "expanded"
when: !root.hasImage && root.expanded
AnchorChanges {
anchors.verticalCenter: undefined
anchors.top: image.parent.top
}
}
}
StyledText {
id: summary
anchors.top: parent.top
anchors.left: image.right
anchors.leftMargin: Appearance.spacing.small
text: root.modelData.summary
maximumLineCount: 1
}
StyledText {
id: timeSep
anchors.top: parent.top
anchors.left: summary.right
anchors.leftMargin: Appearance.spacing.small
text: "•"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
StyledText {
id: time
anchors.top: parent.top
anchors.left: timeSep.right
anchors.leftMargin: Appearance.spacing.small
animate: true
horizontalAlignment: Text.AlignLeft
text: root.modelData.timeStr
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
StyledRect {
id: expandBtn
anchors.right: parent.right
anchors.top: parent.top
MaterialIcon {
animate: true
text: root.expanded ? "expand_less" : "expand_more"
font.pointSize: Appearance.font.size.smaller
}
}
StyledText {
id: bodyPreview
anchors.left: summary.left
anchors.right: expandBtn.left
anchors.top: summary.bottom
anchors.rightMargin: Appearance.spacing.small
text: bodyPreviewMetrics.elidedText
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
TextMetrics {
id: bodyPreviewMetrics
text: root.modelData.body
font.family: bodyPreview.font.family
font.pointSize: bodyPreview.font.pointSize
elide: Text.ElideRight
elideWidth: bodyPreview.width
}
}
}

View file

@ -12,14 +12,7 @@ Variants {
id: root
required property ShellScreen modelData
property bool osdVisible
Timer {
// running: true
repeat: true
interval: 2000
onTriggered: root.osdVisible = !root.osdVisible
}
property bool osdVisible: Notifs.list.length > 0
LazyLoader {
loading: true
@ -38,7 +31,9 @@ Variants {
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
width: wrapper.width
height: wrapper.height

View file

@ -2,10 +2,12 @@
set -l dbus 'quickshell.dbus.properties.warning = false' # System tray dbus property errors
set -l text_input 'qt.qpa.wayland.textinput.warning = false' # Text input focus when open window
set -l notifs 'quickshell.service.notifications.warning = false' # Notification server warnings on reload
set -l sni 'quickshell.service.sni.host.warning = false' # StatusNotifierItem warnings on reload
set -l hypr 'invalid nullptr parameter' # Error that always pops up on Hyprland
set -l intercept '^qsintercept:.*(:[0-9]+){2}$' # Empty qsintercept lines
set -l loop 'Binding loop detected' # Binding loops
set -l process 'QProcess: Destroyed while process' # Long running processes on reload
set -l async_loader 'items in the process of being created at engine destruction' # Async loaders on reload
qs -c caelestia --log-rules "$dbus;$text_input" | grep -vE -e $hypr -e $intercept -e $loop -e $process -e $async_loader
qs -c caelestia --log-rules "$dbus;$text_input;$notifs;$sni" | grep -vE -e $hypr -e $intercept -e $loop -e $process -e $async_loader

76
services/Notifs.qml Normal file
View file

@ -0,0 +1,76 @@
pragma Singleton
import "root:/config"
import Quickshell
import Quickshell.Services.Notifications
import QtQuick
Singleton {
id: root
readonly property list<Notif> list: []
NotificationServer {
id: server
keepOnReload: false
actionsSupported: true
bodyHyperlinksSupported: true
bodyImagesSupported: true
bodyMarkupSupported: true
imageSupported: true
onNotification: notif => {
notif.tracked = true;
root.list.push(notifComp.createObject(root, {
popup: true,
notification: notif
}));
}
}
component Notif: QtObject {
id: notif
property bool popup
readonly property date time: new Date()
readonly property string timeStr: {
const diff = Time.date.getTime() - time.getTime();
const m = Math.floor(diff / 60000);
const h = Math.floor(m / 60);
if (h < 1 && m < 1)
return "now";
if (h < 1)
return `${m}m`;
return `${h}h`;
}
required property Notification notification
readonly property string summary: notification.summary
readonly property string body: notification.body
readonly property string appIcon: notification.appIcon
readonly property string image: notification.image
readonly property Timer timer: Timer {
running: true
interval: notif.notification.expireTimeout > 0 ? notif.notification.expireTimeout : NotifsConfig.defaultExpireTimeout
onTriggered: notif.popup = false
}
readonly property Connections conn: Connections {
target: notif.notification
function onClosed(): void {
notif.destroy();
}
}
}
Component {
id: notifComp
Notif {}
}
}

15
widgets/Colouriser.qml Normal file
View file

@ -0,0 +1,15 @@
import "root:/config"
import QtQuick
import QtQuick.Effects
MultiEffect {
colorization: 1
Behavior on colorizationColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}