2 Commits
Author SHA1 Message Date
Your Name d4c5dfc135 Test 2026-07-17 20:03:04 +00:00
Your Name 880ee3974f Test 2026-07-17 19:57:48 +00:00
9 changed files with 630 additions and 2 deletions
+22
View File
@@ -105,6 +105,28 @@ download \
"$SCRIPT_DIR/modules/jl/assets/correct.svg" \
"correct.svg"
# ----------------------------
# Notification sounds
# ----------------------------
echo
info "Preparing notification sounds..."
mkdir -p "$SCRIPT_DIR/assets/sounds"
download \
"https://raw.githubusercontent.com/deepin-community/sound-theme-freedesktop/master/stereo/dialog-information.oga" \
"$SCRIPT_DIR/assets/sounds/notification-low.oga" \
"notification-low.oga"
download \
"https://raw.githubusercontent.com/deepin-community/sound-theme-freedesktop/master/stereo/dialog-warning.oga" \
"$SCRIPT_DIR/assets/sounds/notification-normal.oga" \
"notification-normal.oga"
download \
"https://raw.githubusercontent.com/deepin-community/sound-theme-freedesktop/master/stereo/bell.oga" \
"$SCRIPT_DIR/assets/sounds/notification-critical.oga" \
"notification-critical.oga"
# ----------------------------
# Nerd Font
# ----------------------------
+5
View File
@@ -3,10 +3,13 @@ import Quickshell
import Quickshell.Hyprland
import "./widgets"
import "../../config"
import "../notifications"
PanelWindow {
id: panel
required property var manager
color: "transparent"
anchors.top: true
@@ -47,6 +50,8 @@ PanelWindow {
WindowInfo {}
NotificationIndicator {manager: panel.manager}
}
Row { // bar widgets center
+4 -1
View File
@@ -4,11 +4,14 @@ import Quickshell
Item {
id: bar_root
required property var manager
Repeater { // Bars
model: Quickshell.screens
delegate: Loader {
active: true // loads object
sourceComponent: Bar {screen: modelData} // creates instance of bar component
sourceComponent: Bar {screen: modelData; manager: root.manager} // creates instance of bar component
}
}
}
+154
View File
@@ -0,0 +1,154 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "../../config"
Rectangle {
id: card
required property var notification
required property var manager
width: parent.width
height: Math.max(64, cardContent.height + 24)
radius: Config.radius
property var urgency: notification.urgency
property bool isCritical: urgency === NotificationUrgency.Critical
color: {
if (isCritical) return Qt.alpha(Colors.errorContainer, 0.95)
if (urgency === NotificationUrgency.Low) return Qt.alpha(Colors.surfaceContainerHigh, 0.95)
return Qt.alpha(Colors.primaryContainer, 0.95)
}
border {
width: Config.border_width
color: {
if (isCritical) return Colors.error
if (urgency === NotificationUrgency.Low) return Colors.outline
return Colors.primaryContainer_fg
}
}
NumberAnimation on opacity {
id: fadeInAnim
from: 0
to: 1
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation on x {
id: slideInAnim
from: parent ? parent.width : 100
to: 0
duration: 300
easing.type: Easing.OutCubic
}
Component.onCompleted: {
fadeInAnim.start()
slideInAnim.start()
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onClicked: {
manager.dismissNotification(notification)
}
}
Row {
id: cardContent
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 12
}
spacing: 10
Item {
id: iconContainer
width: 28
height: 28
anchors.verticalCenter: parent.verticalCenter
Image {
id: notifIcon
anchors.fill: parent
source: notification.appIcon ? "file://" + notification.appIcon : ""
sourceSize.width: 28
sourceSize.height: 28
asynchronous: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
text: "\uF0F3"
font.pixelSize: 16
font.family: Config.font
visible: !notifIcon.visible
color: Colors.primary_fg
}
}
Column {
id: textColumn
width: parent.width - iconContainer.width - parent.spacing - 20
spacing: 3
anchors.verticalCenter: parent.verticalCenter
Text {
id: summaryText
width: parent.width
text: notification.summary || notification.appName || "Notification"
font.pixelSize: 13
font.bold: true
font.family: Config.font
color: {
if (isCritical) return Colors.error
if (urgency === NotificationUrgency.Low) return Colors.secondaryContainer_fg
return Colors.primary
}
elide: Text.ElideRight
maximumLineCount: 1
}
Text {
id: bodyText
width: parent.width
text: notification.body || ""
visible: text !== ""
wrapMode: Text.WordWrap
font.pixelSize: 11
font.family: Config.font
color: isCritical ? Colors.errorContainer_fg : Colors.surface_fg
maximumLineCount: 2
elide: Text.ElideRight
}
}
Text {
text: "\u00D7"
font.pixelSize: 16
font.bold: true
color: isCritical ? Colors.error : Colors.secondary
anchors.verticalCenter: parent.verticalCenter
opacity: 0.6
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
manager.dismissNotification(notification)
}
}
}
}
}
@@ -0,0 +1,233 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "../../config"
MouseArea {
id: indicator
required property var manager
height: bar.height
width: indicatorRow.width + Config.spacing_fun(height) * 2
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
property bool popupOpen: false
onClicked: {
popupOpen = !popupOpen
if (popupOpen) {
notifPopup.visible = true
} else {
notifPopup.visible = false
}
}
Rectangle {
anchors.fill: parent
color: indicator.containsMouse || popupOpen ? Colors.tertiaryContainer : "transparent"
radius: Config.radius_fun(indicator.height)
border.width: (indicator.containsMouse || popupOpen) ? Config.border_width : 0
border.color: (indicator.containsMouse || popupOpen) ? Colors.tertiaryContainer_fg : "transparent"
Row {
id: indicatorRow
anchors.centerIn: parent
spacing: Config.spacing_fun(indicator.height) / 2
Item {
id: iconWrap
width: Config.iconSize(indicator.height)
height: Config.iconSize(indicator.height)
anchors.verticalCenter: parent.verticalCenter
Image {
id: notifIcon
anchors.fill: parent
source: (manager.lastNotification && manager.lastNotification.appIcon)
? "file://" + manager.lastNotification.appIcon : ""
sourceSize.width: Config.iconSize(indicator.height)
sourceSize.height: Config.iconSize(indicator.height)
asynchronous: true
visible: status === Image.Ready
}
Text {
anchors.centerIn: parent
text: "\uF0F3"
font.pixelSize: Config.iconSize(indicator.height)
font.family: Config.font
visible: !notifIcon.visible
color: indicator.containsMouse ? Colors.tertiary : Colors.secondary
}
}
Rectangle {
id: countBadge
width: Math.max(badgeText.width + 6, 16)
height: 16
radius: 8
visible: manager.notificationCount > 0
anchors.verticalCenter: parent.verticalCenter
color: manager.hasCritical ? Colors.error : Colors.primary
Text {
id: badgeText
anchors.centerIn: parent
text: manager.notificationCount > 99 ? "99+" : manager.notificationCount.toString()
font.pixelSize: Config.font_size_tiny(indicator.height)
font.family: Config.font
font.bold: true
color: manager.hasCritical ? Colors.error_fg : Colors.primary_fg
}
}
Rectangle {
id: criticalDot
width: 8
height: 8
radius: 4
visible: manager.hasCritical
anchors.verticalCenter: parent.verticalCenter
color: Colors.error
SequentialAnimation on opacity {
loops: Animation.Infinite
PropertyAnimation { from: 1.0; to: 0.4; duration: 800 }
PropertyAnimation { from: 0.4; to: 1.0; duration: 800 }
}
}
}
}
PopupWindow {
id: notifPopup
visible: indicator.popupOpen
color: "transparent"
anchor {
item: indicator
edges: Edges.Top | Edges.Left
gravity: Edges.Bottom | Edges.Right
adjustment: PopupAdjustment.FlipY | PopupAdjustment.SlideX
onAnchoring: {
anchor.rect.x = (indicator.width - notifPopup.width) / 2
anchor.rect.y = indicator.height + 4
}
}
implicitWidth: 280
implicitHeight: Math.max(60, popupContent.height + 24)
Rectangle {
anchors.fill: parent
color: Qt.alpha(Colors.primaryContainer, 0.95)
radius: Config.radius
border {
width: Config.border_width
color: Colors.primaryContainer_fg
}
Column {
id: popupContent
width: parent.width
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 12
}
spacing: 8
Text {
text: "Notifications"
font.pixelSize: Config.font_size_small(indicator.height)
font.family: Config.font
font.bold: true
color: Colors.primary
anchors.horizontalCenter: parent.horizontalCenter
visible: manager.notificationCount > 0
}
Repeater {
model: manager.notifications
delegate: Rectangle {
width: parent.width
height: delegateText.height + 10
radius: Config.small_radius
color: Qt.alpha(Colors.surfaceContainerHighest, 0.5)
property var notif: modelData
Row {
anchors {
left: parent.left
right: parent.right
margins: 6
verticalCenter: parent.verticalCenter
}
spacing: 6
Rectangle {
width: 6
height: 24
radius: 3
color: {
if (notif.urgency === NotificationUrgency.Critical) return Colors.error
if (notif.urgency === NotificationUrgency.Low) return Colors.outline
return Colors.primary
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - 24
spacing: 2
Text {
id: delegateText
width: parent.width
text: notif.summary || notif.appName || "Notification"
font.pixelSize: Config.labelSize(indicator.height)
font.family: Config.font
font.bold: true
color: Colors.surface_fg
elide: Text.ElideRight
maximumLineCount: 1
}
Text {
width: parent.width
text: notif.body || ""
visible: text !== ""
font.pixelSize: Config.font_size_tiny(indicator.height)
font.family: Config.font
color: Colors.surfaceVariant_fg
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
}
}
Text {
text: "No notifications"
font.pixelSize: Config.font_size_small(indicator.height)
font.family: Config.font
color: Colors.secondary
anchors.horizontalCenter: parent.horizontalCenter
visible: manager.notificationCount === 0
height: 30
}
}
}
onVisibleChanged: {
if (!visible) indicator.popupOpen = false
}
}
}
@@ -0,0 +1,129 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Notifications
import "../../config"
Item {
id: root
NotificationServer {
id: notificationServer
imageSupported: true
actionsSupported: true
bodyMarkupSupported: false
onNotification: (notification) => {
notification.tracked = true
root.onNotificationReceived(notification)
}
}
readonly property ObjectModel notifications: notificationServer.trackedNotifications
property int notificationCount: 0
property var lastNotification: null
property bool hasCritical: false
readonly property string soundDir: Config.configDir + "/assets/sounds"
property var _trackedIds: ({})
property var _dismissTimes: ({})
function onNotificationReceived(notification) {
_trackedIds[notification.id] = true
notificationCount = Object.keys(_trackedIds).length
lastNotification = notification
if (notification.urgency === NotificationUrgency.Critical)
hasCritical = true
playSound(notification.urgency)
scheduleDismiss(notification)
}
function playSound(urgency) {
var path
switch (urgency) {
case NotificationUrgency.Low:
path = soundDir + "/notification-low.oga"
break
case NotificationUrgency.Critical:
path = soundDir + "/notification-critical.oga"
break
default:
path = soundDir + "/notification-normal.oga"
break
}
soundProc.command = [
"sh", "-c",
"pw-play \"" + path + "\" 2>/dev/null || paplay \"" + path + "\" 2>/dev/null || ffplay -nodisp -autoexit \"" + path + "\" 2>/dev/null"
]
soundProc.running = true
}
function scheduleDismiss(notification) {
var timeout = notification.expireTimeout
if (timeout <= 0) {
if (notification.urgency === NotificationUrgency.Critical)
timeout = 60
else
timeout = 5
}
_dismissTimes[notification.id] = Date.now() / 1000 + timeout
if (!refreshTimer.running)
refreshTimer.running = true
}
function dismissNotification(notification) {
delete _trackedIds[notification.id]
delete _dismissTimes[notification.id]
notificationCount = Object.keys(_trackedIds).length
notification.tracked = false
refreshMetadata()
}
function refreshMetadata() {
hasCritical = false
lastNotification = null
for (var i = 0; i < notifications.count; i++) {
var n = notifications.get(i)
if (_trackedIds[n.id]) {
lastNotification = n
if (n.urgency === NotificationUrgency.Critical)
hasCritical = true
}
}
if (notificationCount === 0) {
hasCritical = false
lastNotification = null
}
}
Timer {
id: refreshTimer
interval: 1000
repeat: true
onTriggered: {
var now = Date.now() / 1000
for (var id in _dismissTimes) {
if (_dismissTimes[id] <= now) {
for (var i = 0; i < notifications.count; i++) {
var n = notifications.get(i)
if (n.id.toString() === id) {
dismissNotification(n)
break
}
}
}
}
if (Object.keys(_trackedIds).length === 0)
running = false
}
}
Process {
id: soundProc
running: false
}
}
@@ -0,0 +1,71 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import Quickshell.Services.Notifications
import "../../config"
Variants {
id: root
required property var manager
model: Quickshell.screens
delegate: PanelWindow {
id: popupWindow
screen: modelData
color: "transparent"
anchors.top: true
anchors.right: true
width: 380
height: Math.min(implicitHeight, screen.height * 0.6)
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "notification-popup"
margins {
top: 8
right: 8
}
readonly property var hyprMonitor: Hyprland.monitorFor(screen)
visible: hyprMonitor === Hyprland.focusedMonitor
implicitHeight: contentColumn.height
Connections {
target: Hyprland
function onFocusedMonitorChanged() {
popupWindow.visible = popupWindow.hyprMonitor === Hyprland.focusedMonitor
}
}
Connections {
target: root.manager.notifications
function onCountChanged() {
popupWindow.visible = popupWindow.hyprMonitor === Hyprland.focusedMonitor
&& root.manager.notifications.count > 0
}
}
Column {
id: contentColumn
width: parent.width
spacing: 8
topPadding: 4
Repeater {
model: root.manager.notifications
delegate: NotificationCard {
width: contentColumn.width
notification: modelData
manager: root.manager
}
}
}
}
}
+4
View File
@@ -0,0 +1,4 @@
NotificationManager 1.0 NotificationManager.qml
NotificationPopup 1.0 NotificationPopup.qml
NotificationIndicator 1.0 NotificationIndicator.qml
NotificationCard 1.0 NotificationCard.qml
+8 -1
View File
@@ -3,17 +3,24 @@
import Quickshell //for PanelWindow
import Quickshell.Hyprland
import QtQuick //for Text
import Quickshell.Services.Notifications
import "./modules/bar"
import "./modules/wp"
import "./modules/wlogout"
import "./modules/jl"
import "./modules/background"
import "./modules/notifications"
ShellRoot {
id: root
// Notifications
NotificationManager { id: notificationManager }
NotificationPopup { manager: notificationManager }
// Bar
BarLoader {}
BarLoader { manager: notificationManager }
//Wallpaper picker
WallpaperPickerLoader {}