notifications working (totally) perfectly

This commit is contained in:
2026-06-16 22:01:15 +01:00
parent 23407ea36b
commit 603da98035
9 changed files with 598 additions and 79 deletions

View File

@@ -1,20 +1,25 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Services.Notifications
import "../Color.js" as Colors import "../Color.js" as Colors
import "../Components/" import "../Components/"
import "../Services/" import "../Services/"
import "./Center"
Container { Container {
id: centerPill id: root
property Notification latestNotification: NotificationManager.getLatestNotification() || null
required property HyprlandMonitor monitor required property HyprlandMonitor monitor
property NotificationServer notifServer: NotificationManager.notif
property string previousState property string previousState
boxColor: Colors.mauve boxColor: Colors.mauve
defaultItem: time defaultItem: time
exclusiveMonitor: centerPill.monitor exclusiveMonitor: root.monitor
exclusiveToScreen: true exclusiveToScreen: true
state: "" state: ""
@@ -23,17 +28,18 @@ Container {
name: "" name: ""
PropertyChanges { PropertyChanges {
centerPill.boxHeight: 28 root.boxHeight: 28
centerPill.boxRadius: 9 root.boxRadius: 9
centerPill.boxWidth: 100 root.boxWidth: 100
root.visibleTopMargin: 0
} }
StateChangeScript { StateChangeScript {
script: { script: {
if (centerPill.previousState != "hovered") { if (root.previousState != "hovered") {
centerPill.stack.replace(time); root.stack.replace(time);
} }
centerPill.previousState = ""; root.previousState = "";
} }
} }
}, },
@@ -41,14 +47,15 @@ Container {
name: "hovered" name: "hovered"
PropertyChanges { PropertyChanges {
centerPill.boxHeight: 35 root.boxHeight: 35
centerPill.boxRadius: 15 root.boxRadius: 15
centerPill.boxWidth: 110 root.boxWidth: 110
root.visibleTopMargin: 0
} }
StateChangeScript { StateChangeScript {
script: { script: {
centerPill.previousState = "hovered"; root.previousState = "hovered";
} }
} }
}, },
@@ -56,28 +63,74 @@ Container {
name: "expanded" name: "expanded"
PropertyChanges { PropertyChanges {
centerPill.boxHeight: 140 root.boxHeight: 140
centerPill.boxRadius: 30 root.boxRadius: 30
centerPill.boxWidth: 300 root.boxWidth: 300
root.visibleTopMargin: 0
} }
StateChangeScript { StateChangeScript {
script: { script: {
centerPill.stack.replace(fullTime); if (root.state != "notifications") {
centerPill.previousState = "expanded"; root.stack.replace(fullTime);
root.previousState = "expanded";
}
}
}
},
State {
extend: "expanded"
name: "notifications"
PropertyChanges {
root.boxHeight: 320
}
},
State {
name: "notified"
PropertyChanges {
root.boxHeight: 90
root.boxRadius: 30
root.boxWidth: 330
root.visibleTopMargin: 20
}
StateChangeScript {
script: {
root.stack.replace(notification);
root.previousState = "notified";
} }
} }
} }
] ]
mouse.onClicked: { hover.onHoveredChanged: {
centerPill.state == "expanded" ? centerPill.state = "" : centerPill.state = "expanded"; hover.hovered == true ? root.state == "notified" ? root.state = "notified" : root.state = "hovered" :
root.state = "";
} }
mouse.onEntered: { onLatestNotificationChanged: {
centerPill.state = "hovered"; if (latestNotification != null && Hyprland.focusedMonitor == monitor) {
if (root.latestNotification.lastGeneration == false) {
root.state = "notified";
notificationViewTimer.start();
}
}
} }
mouse.onExited: { tap.onTapped: {
centerPill.state = ""; root.state == "hovered" ? root.state = "expanded" : undefined;
}
Timer {
id: notificationViewTimer
interval: 3000
repeat: false
running: false || !root.hover.hovered
onTriggered: {
root.state = "";
}
} }
Component { Component {
@@ -93,50 +146,42 @@ Container {
Component { Component {
id: fullTime id: fullTime
Item { Expanded {
Rectangle { NotificationList {
anchors.fill: parent id: notifList
radius: 30
gradient: Gradient { animEnabled: false
GradientStop { radius: root.boxRadius
color: "transparent" rootState: root.state
position: 0.0 server: root.notifServer
} state: ""
GradientStop { TapHandler {
color: "transparent" onTapped: {
position: 0.6 notifList.animEnabled = true;
} root.state = "notifications";
Qt.callLater(() => {
GradientStop { notifList.animEnabled = false;
color: Colors.red });
position: 1.0
}
}
}
Column {
anchors.centerIn: parent
spacing: 5
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: 30
text: Time.time
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.surface0
text: Time.date
font {
pointSize: 9
weight: 400
} }
} }
} }
} }
} }
Component {
id: notification
NotificationDisplay {
body: root.latestNotification.body
summary: root.latestNotification.summary
TapHandler {
onTapped: {
root.state = "notifications";
root.stack.replace(fullTime);
}
}
}
}
} }

55
Bar/Center/Expanded.qml Normal file
View File

@@ -0,0 +1,55 @@
import QtQuick
import Quickshell
import "../../Color.js" as Colors
import "../../Components/"
import "../../Services/"
Item {
id: fullTimeRoot
Rectangle {
anchors.fill: parent
radius: 30
gradient: MidpointGradient {
color: Colors.red
}
}
Item {
id: timeColumnContainer
anchors.fill: parent
Column {
id: timeColumn
spacing: 5
anchors {
centerIn: parent
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.surface0
text: Time.time
font {
pointSize: 30
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.surface1
text: Time.date
font {
pointSize: 9
weight: 400
}
}
}
}
}

View File

@@ -0,0 +1,47 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "../../Color.js" as Colors
import "../../Components/"
import "../../Services/"
Item {
id: root
required property string body
required property string summary
Rectangle {
anchors.fill: parent
radius: 30
gradient: MidpointGradient {
color: Colors.green
midpoint: 0.7
}
}
Column {
anchors.centerIn: parent
spacing: 5
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: 11
text: root.summary
}
StyledText {
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: root.body
verticalAlignment: Text.AlignVCenter
width: root.width - 20
font {
pointSize: 10
weight: 500
}
}
}
}

View File

@@ -0,0 +1,302 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Services.Notifications
import "../../Color.js" as Colors
import "../../Components/"
import "../../Services/"
Item {
id: root
required property bool animEnabled
required property double radius
required property string rootState
required property NotificationServer server
Behavior on anchors.topMargin {
enabled: root.animEnabled
SpringAnimation {
damping: 0.35
spring: 4
}
}
states: [
State {
name: ""
when: root.rootState != "notifications"
PropertyChanges {
root.anchors.topMargin: root.parent.height - 21
}
},
State {
name: "expanded"
when: root.rootState == "notifications"
PropertyChanges {
root.anchors.topMargin: 5
}
}
]
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
top: parent.top
topMargin: parent.height - 21
}
Rectangle {
id: background
anchors.fill: parent
color: Colors.surface0
radius: root.radius - 3
states: [
State {
name: ""
when: root.rootState != "notifications"
PropertyChanges {}
},
State {
name: "expanded"
when: root.rootState == "notifications"
PropertyChanges {
background.anchors.bottomMargin: 5
background.anchors.leftMargin: 5
background.anchors.rightMargin: 5
}
StateChangeScript {
script: {
console.log("some");
}
}
}
]
anchors {
bottomMargin: 15
leftMargin: 20
rightMargin: 20
}
}
Item {
id: notifListContainer
anchors.fill: parent
clip: true
opacity: 0
anchors {
bottomMargin: 5
leftMargin: 5
rightMargin: 5
topMargin: 5
}
CenteredText {
color: Colors.overlay0
text: "No Notifications"
visible: root.server.trackedNotifications.values.length <= 0
}
ListView {
id: notifScrollingList
anchors.fill: parent
boundsBehavior: Flickable.StopAtBounds
height: Math.min(contentHeight, parent.height)
implicitHeight: background.height
model: root.server.trackedNotifications.values.length
spacing: 7
// verticalLayoutDirection: ListView.BottomToTop
delegate: Item {
id: notifRoot
property Notification currentNotif: root.server.trackedNotifications.values[(notifRoot.notifLen
- 1) - index]
required property real index
property real notifLen: root.server.trackedNotifications.values.length
height: 50
width: ListView.view.width
states: [
State {
name: ""
when: notifHover.hovered == false
},
State {
name: "hovered"
when: notifHover.hovered == false
PropertyChanges {}
}
]
HoverHandler {
id: notifHover
}
Rectangle {
id: dismissButton
color: Colors.red
implicitWidth: 100
radius: 10
Behavior on anchors.rightMargin {
NumberAnimation {
duration: 100
}
}
states: [
State {
name: ""
when: notifHover.hovered == false
},
State {
name: "hovered"
when: notifHover.hovered == true
PropertyChanges {
dismissButton.anchors.rightMargin: 5
}
}
]
TapHandler {
onTapped: {
notifRoot.currentNotif.dismiss();
}
}
anchors {
bottom: parent.bottom
bottomMargin: 3
right: parent.right
rightMargin: 10
top: parent.top
topMargin: 3
}
}
Rectangle {
id: textContainer
anchors.fill: parent
clip: true
color: Colors.surface2
radius: 9
Behavior on anchors.rightMargin {
NumberAnimation {
duration: 100
}
}
states: [
State {
name: ""
when: notifHover.hovered == false
},
State {
name: "hovered"
when: notifHover.hovered == true
PropertyChanges {
textContainer.anchors.rightMargin: 40
}
}
]
Column {
id: textColumn
anchors {
centerIn: parent
left: parent.left
right: parent.right
}
StyledText {
color: Colors.text
horizontalAlignment: Text.AlignHCenter
text: notifRoot.currentNotif.summary
verticalAlignment: Text.AlignVCenter
width: notifRoot.width - 10
}
StyledText {
color: Colors.subtext1
horizontalAlignment: Text.AlignHCenter
text: notifRoot.currentNotif.body
verticalAlignment: Text.AlignVCenter
width: notifRoot.width - 10
}
}
}
}
anchors {
bottomMargin: 5
leftMargin: 10
rightMargin: 10
topMargin: 5
}
}
}
MultiEffect {
id: notifListClipper
anchors.fill: notifListContainer
maskEnabled: true
maskSource: clipsource
opacity: 0
source: notifListContainer
Behavior on opacity {
NumberAnimation {
duration: 100
}
}
states: [
State {
name: ""
when: root.rootState != "notifications"
PropertyChanges {
notifListClipper.opacity: 0
}
},
State {
name: "expanded"
when: root.rootState == "notifications"
PropertyChanges {
notifListClipper.opacity: 1
}
}
]
}
Rectangle {
id: clipsource
anchors.fill: background
color: "black"
layer.enabled: true
radius: background.radius
visible: false
}
}

View File

@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
@@ -12,26 +13,26 @@ Item {
property double boxHeight: 28 property double boxHeight: 28
property double boxRadius: 9 property double boxRadius: 9
property double boxWidth: 100 property double boxWidth: 100
property Component defaultItem readonly property double calculatedTopMargin: {
required property HyprlandMonitor exclusiveMonitor
property bool exclusiveToScreen: false
property alias mouse: mouseArea
property alias rect: container
property alias stack: containerContent
function getVisible() {
if (containerRoot.exclusiveToScreen) { if (containerRoot.exclusiveToScreen) {
if (Hyprland.focusedMonitor != containerRoot.exclusiveMonitor) { if (Hyprland.focusedMonitor != containerRoot.exclusiveMonitor) {
return -4 - containerRoot.boxHeight - 5; return -4 - containerRoot.boxHeight - 5;
} else { } else {
return 0; return visibleTopMargin;
} }
} else { } else {
return 0; return visibleTopMargin;
} }
} }
property Component defaultItem
required property HyprlandMonitor exclusiveMonitor
property bool exclusiveToScreen: false
property alias hover: hoverHandler
property alias rect: container
property alias stack: containerContent
property alias tap: tapHandler
property double visibleTopMargin: 0
clip: true
implicitHeight: boxHeight implicitHeight: boxHeight
implicitWidth: boxWidth implicitWidth: boxWidth
@@ -59,7 +60,7 @@ Item {
anchors { anchors {
top: parent.top top: parent.top
topMargin: getVisible() topMargin: calculatedTopMargin
} }
NumberAnimation { NumberAnimation {
@@ -69,18 +70,29 @@ Item {
easing: Easing.InOutBack easing: Easing.InOutBack
} }
MouseArea { HoverHandler {
id: mouseArea id: hoverHandler
}
anchors.fill: parent TapHandler {
hoverEnabled: true id: tapHandler
z: 1 }
RectangularShadow {
anchors.fill: container
blur: 30
color: Colors.mantle
offset.x: 7
offset.y: 3
radius: container.radius
spread: 10
} }
Rectangle { Rectangle {
id: container id: container
anchors.fill: parent anchors.fill: parent
clip: true
color: containerRoot.boxColor color: containerRoot.boxColor
radius: containerRoot.boxRadius radius: containerRoot.boxRadius

View File

@@ -0,0 +1,23 @@
import QtQuick
Gradient {
id: root
required property color color
property double midpoint: 0.6
GradientStop {
color: "transparent"
position: 0.0
}
GradientStop {
color: "transparent"
position: root.midpoint
}
GradientStop {
color: root.color
position: 1.0
}
}

View File

@@ -3,6 +3,7 @@ import "../Color.js" as Colors
Text { Text {
color: Colors.base color: Colors.base
elide: Text.ElideRight
font { font {
family: "FiraMono Nerd Font" family: "FiraMono Nerd Font"

View File

@@ -0,0 +1,31 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
Singleton {
id: root
property alias notif: server
function getLatestNotification() {
let notificationList = server.trackedNotifications.values;
let len = notificationList.length;
if (len <= 0) {
len = 1;
}
let latestNotification = notificationList[len - 1];
return latestNotification;
}
NotificationServer {
id: server
bodyMarkupSupported: true
bodySupported: true
onNotification: notification => {
notification.tracked = true;
}
}
}

3
Services/qmldir Normal file
View File

@@ -0,0 +1,3 @@
module Services
singleton Time 1.0 Time.qml
singleton NotificationManager 1.0 NotificationManager.qml