move NotifService and NotifItem to services/, keep notification UI in modules/

This commit is contained in:
Damocles 2026-04-18 00:26:14 +02:00
parent a17a365b81
commit d20fdf8fa0
8 changed files with 33 additions and 36 deletions

View file

@ -33,8 +33,8 @@ M.HoverPanel {
// DND toggle
Text {
text: M.NotifService.dnd ? "\uDB82\uDE93" : "\uDB80\uDC9C"
color: M.NotifService.dnd ? S.Theme.base09 : S.Theme.base04
text: S.NotifService.dnd ? "\uDB82\uDE93" : "\uDB80\uDC9C"
color: S.NotifService.dnd ? S.Theme.base09 : S.Theme.base04
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
@ -42,7 +42,7 @@ M.HoverPanel {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: M.NotifService.toggleDnd()
onClicked: S.NotifService.toggleDnd()
}
}
@ -53,7 +53,7 @@ M.HoverPanel {
font.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
visible: M.NotifService.count > 0
visible: S.NotifService.count > 0
MouseArea {
id: clearArea
@ -88,7 +88,7 @@ M.HoverPanel {
// Group notifications by appName, sorted by max urgency desc then most recent time desc
readonly property var _groups: {
const map = {};
for (const n of M.NotifService.list) {
for (const n of S.NotifService.list) {
const key = n.appName || "";
if (!map[key])
map[key] = {
@ -167,14 +167,14 @@ M.HoverPanel {
}
function _cascadeDismiss() {
if (M.NotifService.list.length === 0)
if (S.NotifService.list.length === 0)
return;
const ids = M.NotifService.list.map(n => n.id);
const ids = S.NotifService.list.map(n => n.id);
_startCascade(_getVisibleNotifDelegates(), ids);
}
function _cascadeGroupDismiss(appName) {
const ids = M.NotifService.list.filter(n => n.appName === appName).map(n => n.id);
const ids = S.NotifService.list.filter(n => n.appName === appName).map(n => n.id);
if (ids.length === 0)
return;
_startCascade(_getVisibleNotifDelegates(appName), ids);
@ -184,7 +184,7 @@ M.HoverPanel {
const ids = _pendingDismissIds;
_pendingDismissIds = [];
for (const id of ids)
M.NotifService.dismiss(id);
S.NotifService.dismiss(id);
}
property Component _cascadeTimer: Component {
@ -467,7 +467,7 @@ M.HoverPanel {
ScriptAction {
script: {
if (notifDelegate._notif && !notifDelegate._skipDismiss)
M.NotifService.dismiss(notifDelegate._notif.id);
S.NotifService.dismiss(notifDelegate._notif.id);
}
}
}
@ -476,7 +476,7 @@ M.HoverPanel {
// Empty state
Text {
visible: M.NotifService.count === 0
visible: S.NotifService.count === 0
width: menuWindow.contentWidth
height: 48
horizontalAlignment: Text.AlignHCenter

View file

@ -1,65 +0,0 @@
import QtQuick
import Quickshell.Services.Notifications
import "." as M
import "../services" as S
QtObject {
id: root
property bool popup: false
property string state: "visible" // "visible" | "dismissing" | "dismissed"
property var notification: null
property var id
property string summary
property string body
property string appName
property string appIcon
property string image
property var hints
property int urgency: NotificationUrgency.Normal
property var actions: []
property real time: Date.now()
// Expire timer owned by this item, not dynamically created
readonly property Timer _expireTimer: Timer {
running: false
onTriggered: {
if (root.state === "visible")
root.popup = false;
}
}
// Relative time string recomputed whenever NotifService._now ticks (single global 5s timer)
readonly property string timeStr: {
const diff = M.NotifService._now - time;
const m = Math.floor(diff / 60000);
if (m < 1)
return "now";
const h = Math.floor(m / 60);
if (h < 1)
return m + "m";
const d = Math.floor(h / 24);
return d > 0 ? d + "d" : h + "h";
}
// App closed the notification from its side remove from our list while the object is still alive
readonly property Connections _notifConn: Connections {
target: root.notification
function onClosed() {
if (root.state !== "dismissed")
M.NotifService.dismiss(root.id);
}
}
function beginDismiss() {
if (state === "visible")
state = "dismissing";
}
function finishDismiss() {
state = "dismissed";
_expireTimer.running = false;
notification?.dismiss();
}
}

View file

@ -10,7 +10,7 @@ PanelWindow {
required property var screen
visible: M.NotifService.popups.length > 0 && !S.NiriIpc.overviewOpen
visible: S.NotifService.popups.length > 0 && !S.NiriIpc.overviewOpen
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
@ -34,7 +34,7 @@ PanelWindow {
property var _knownIds: ({})
Repeater {
model: M.NotifService.popups.slice(0, S.Modules.notifications.maxPopups || 4)
model: S.NotifService.popups.slice(0, S.Modules.notifications.maxPopups || 4)
delegate: Item {
id: popupItem
@ -74,7 +74,7 @@ PanelWindow {
}
Connections {
target: M.NotifService
target: S.NotifService
function onPopupExpiring(notifId) {
if (notifId === popupItem.modelData.id)
popupItem.animateDismiss(false);
@ -152,7 +152,7 @@ PanelWindow {
easing.type: Easing.OutCubic
}
ScriptAction {
script: popupItem._fullDismiss ? M.NotifService.dismiss(popupItem.modelData.id) : M.NotifService.dismissPopup(popupItem.modelData.id)
script: popupItem._fullDismiss ? S.NotifService.dismiss(popupItem.modelData.id) : S.NotifService.dismissPopup(popupItem.modelData.id)
}
}

View file

@ -1,138 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "." as M
import "../services" as S
QtObject {
id: root
property var list: []
property bool dnd: false
readonly property var popups: list.filter(n => n.popup && n.state !== "dismissed")
readonly property int count: list.filter(n => n.state !== "dismissed").length
// O(1) lookup
property var _byId: ({})
function dismiss(notifId) {
const item = _byId[notifId];
if (!item)
return;
item.finishDismiss();
list = list.filter(n => n !== item);
delete _byId[notifId];
item.destroy();
}
function dismissAll() {
for (const item of list.slice()) {
item.finishDismiss();
delete _byId[item.id];
item.destroy();
}
list = [];
}
function dismissPopup(notifId) {
const item = _byId[notifId];
if (item) {
item.popup = false;
_changed();
}
}
function toggleDnd() {
dnd = !dnd;
}
function _changed() {
list = list.slice();
}
// Signal popups to animate out before removal
signal popupExpiring(var notifId)
property NotificationServer _server: NotificationServer {
actionsSupported: true
bodyMarkupSupported: true
imageSupported: true
persistenceSupported: true
keepOnReload: false
onNotification: notif => {
notif.tracked = true;
const isCritical = notif.urgency === NotificationUrgency.Critical;
const item = _itemComp.createObject(root, {
notification: notif,
id: notif.id,
summary: notif.summary,
body: notif.body,
appName: notif.appName,
appIcon: notif.appIcon,
image: notif.image,
hints: notif.hints,
urgency: notif.urgency,
actions: notif.actions ? notif.actions.map(a => ({
identifier: a.identifier,
text: a.text,
invoke: () => a.invoke()
})) : [],
time: Date.now(),
popup: isCritical || !root.dnd
});
root._byId[item.id] = item;
root.list = [item, ...root.list].sort((a, b) => {
const aU = a.urgency === NotificationUrgency.Critical ? 1 : 0;
const bU = b.urgency === NotificationUrgency.Critical ? 1 : 0;
if (aU !== bU)
return bU - aU;
return b.time - a.time;
});
// Trim excess popups
const max = S.Modules.notifications.maxPopups || 4;
const currentPopups = root.list.filter(n => n.popup);
if (currentPopups.length > max) {
for (let i = max; i < currentPopups.length; i++)
currentPopups[i].popup = false;
root._changed();
}
// Auto-expire popup (skip for critical)
if (item.popup && !isCritical) {
const timeout = notif.expireTimeout > 0 ? notif.expireTimeout : (S.Modules.notifications.timeout || 3000);
item._expireTimer.interval = timeout;
item._expireTimer.running = true;
}
// Trim history (-1 = unlimited)
const maxHistory = S.Modules.notifications.maxHistory ?? -1;
while (maxHistory > 0 && root.list.length > maxHistory) {
const old = root.list.pop();
old.finishDismiss();
delete root._byId[old.id];
old.destroy();
}
}
}
property Component _itemComp: Component {
NotifItem {}
}
// Single global tick for all NotifItem.timeStr bindings replaces per-item 5s timers
property real _now: Date.now()
property Timer _nowTimer: Timer {
running: root.count > 0
repeat: true
interval: 5000
onTriggered: root._now = Date.now()
}
}

View file

@ -8,28 +8,28 @@ M.BarSection {
id: root
spacing: S.Theme.moduleSpacing
tooltip: {
const parts = [M.NotifService.count + " notification" + (M.NotifService.count !== 1 ? "s" : "")];
if (M.NotifService.dnd)
const parts = [S.NotifService.count + " notification" + (S.NotifService.count !== 1 ? "s" : "")];
if (S.NotifService.dnd)
parts.push("Do not disturb");
return parts.join("\n");
}
required property var bar
readonly property bool hasUrgent: M.NotifService.list.some(n => n.urgency === NotificationUrgency.Critical && n.state !== "dismissed")
readonly property bool hasUrgent: S.NotifService.list.some(n => n.urgency === NotificationUrgency.Critical && n.state !== "dismissed")
M.BarIcon {
icon: {
if (M.NotifService.dnd)
return M.NotifService.count > 0 ? "\uDB80\uDCA0" : "\uDB82\uDE93";
return M.NotifService.count > 0 ? "\uDB84\uDD6B" : "\uDB80\uDC9C";
if (S.NotifService.dnd)
return S.NotifService.count > 0 ? "\uDB80\uDCA0" : "\uDB82\uDE93";
return S.NotifService.count > 0 ? "\uDB84\uDD6B" : "\uDB80\uDC9C";
}
color: M.NotifService.dnd ? S.Theme.base04 : root.accentColor
color: S.NotifService.dnd ? S.Theme.base04 : root.accentColor
anchors.verticalCenter: parent.verticalCenter
}
M.BarLabel {
id: countLabel
label: M.NotifService.count > 0 ? String(M.NotifService.count) + (root.hasUrgent ? "!" : "") : ""
label: S.NotifService.count > 0 ? String(S.NotifService.count) + (root.hasUrgent ? "!" : "") : ""
color: root.hasUrgent ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter
@ -61,9 +61,9 @@ M.BarSection {
}
Connections {
target: M.NotifService
target: S.NotifService
function onCountChanged() {
if (M.NotifService.count > 0)
if (S.NotifService.count > 0)
popAnim.start();
}
}
@ -77,7 +77,7 @@ M.BarSection {
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: M.NotifService.toggleDnd()
onTapped: S.NotifService.toggleDnd()
}
LazyLoader {

View file

@ -31,8 +31,6 @@ PowerProfileModule 1.0 PowerProfileModule.qml
IdleInhibitorModule 1.0 IdleInhibitorModule.qml
NotificationsModule 1.0 NotificationsModule.qml
ProcessList 1.0 ProcessList.qml
singleton NotifService 1.0 NotifService.qml
NotifItem 1.0 NotifItem.qml
NotifPopup 1.0 NotifPopup.qml
NotifCenter 1.0 NotifCenter.qml
NotifCard 1.0 NotifCard.qml