Files
Quickbar/Services/NotificationService.qml

196 lines
5.4 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import qs.Common
import qs.Services
Singleton {
id: root
readonly property var notificationServer: notificationServer
readonly property var notificationsNumber: notificationServer.trackedNotifications.values.length
readonly property int maxShown: 3
property bool receivingLock: false
property bool manualNotificationsMuted: false
property bool notificationsMuted: HyprlandService.hasFullscreen || HyprlandService.isScreencasting || root.manualNotificationsMuted
property ListModel trackedNotifications: ListModel {}
NotificationServer {
id: notificationServer
imageSupported: true
}
Connections {
target: notificationServer
function onNotification(notif) {
if (notif.transient || notif.body === "MediaOngoingActivity")
return;
notif.tracked = true;
root.addNotification(root.trackedNotifications, notif);
if (notif.lastGeneration)
return;
// Use the refactored helper
if (notificationList.count < root.maxShown && !root.receivingLock) {
root.addNotification(notificationList, notif);
} else {
root.receivingLock = true;
root.addNotification(pendingList, notif);
}
}
}
ListModel {
id: notificationList
}
ListModel {
id: pendingList
}
/**
* @param {ListModel} model
* @param {var} targetNotif
*/
function removeNotification(model, targetNotif) {
if (!model || typeof model.remove !== "function") {
console.warn("removeNotification(): invalid model");
return;
}
for (let i = 0; i < model.count; i++) {
if (model.get(i).notif === targetNotif) {
model.remove(i);
break;
}
}
}
/**
* @param {ListModel} model
* @param {var} notif
*/
function addNotification(model, notif) {
if (!model || typeof model.append !== "function") {
console.warn("addNotification(): invalid model");
return;
}
// Avoid duplicates
for (let i = 0; i < model.count; i++) {
if (model.get(i).notif === notif)
return;
}
model.append({
notif
});
notif.closed.connect(function (reason) {
removeNotification(model, notif);
});
}
function notificationDismiss(notif) {
let pendingIdx = -1;
for (let i = 0; i < pendingList.count; i++) {
if (pendingList.get(i).notif === notif) {
pendingIdx = i;
break;
}
}
if (pendingIdx >= 0) {
pendingList.remove(pendingIdx, 1);
} else {
removeNotification(notificationList, notif);
}
removeNotification(trackedNotifications, notif);
if (notif && typeof notif.dismiss === "function")
notif.dismiss();
tryShowNext();
}
function timeoutNotification(notif) {
removeNotification(notificationList, notif);
tryShowNext();
}
function tryShowNext() {
let filled = false;
while (notificationList.count < root.maxShown && pendingList.count > 0) {
const nextNotif = pendingList.get(0).notif;
pendingList.remove(0, 1);
addNotification(notificationList, nextNotif);
filled = true;
}
// Only lock if there are still more pending than fit onscreen
root.receivingLock = pendingList.count > 0;
}
LazyLoader {
id: popupLoader
active: notificationList.count && !root.notificationsMuted
component: PanelWindow {
id: notificationWindow
screen: {
Quickshell.screens.filter(screen => screen.name == HyprlandService.focusedMon.name)[0];
}
anchors.top: true
exclusiveZone: 0
implicitWidth: 400
implicitHeight: screen.height
color: "transparent"
mask: Region {
item: listView
}
ListView {
id: listView
model: notificationList
implicitWidth: parent.width
implicitHeight: contentHeight
orientation: ListView.Vertical
clip: true
spacing: 5
interactive: false
delegate: NotificationWrapper {
id: notifWrapper
required property var modelData
notification: modelData
implicitWidth: listView.width
startTimer: NotificationUrgency.toString(notification?.urgency) === "Critical" ? false : true
timerDuration: 5000
onDismissed: {
if (notification && typeof notification.dismiss === "function")
root.notificationDismiss(notification);
}
onTimedout: {
root.timeoutNotification(notification);
}
}
Component.onCompleted: positionViewAtEnd()
}
}
}
}