move NotifService and NotifItem to services/, keep notification UI in modules/
This commit is contained in:
parent
a17a365b81
commit
d20fdf8fa0
8 changed files with 33 additions and 36 deletions
|
|
@ -6,7 +6,6 @@ import Quickshell.Services.Mpris
|
||||||
import Quickshell.Services.Pipewire
|
import Quickshell.Services.Pipewire
|
||||||
import "../services" as S
|
import "../services" as S
|
||||||
import "../applets" as C
|
import "../applets" as C
|
||||||
import "../modules" as M
|
|
||||||
|
|
||||||
WlSessionLockSurface {
|
WlSessionLockSurface {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -149,7 +148,7 @@ WlSessionLockSurface {
|
||||||
visible: (S.Modules.lock.notifications ?? true) && _notifGroups.length > 0
|
visible: (S.Modules.lock.notifications ?? true) && _notifGroups.length > 0
|
||||||
|
|
||||||
readonly property var _notifGroups: {
|
readonly property var _notifGroups: {
|
||||||
const notifs = M.NotifService.list.filter(n => n.state !== "dismissed");
|
const notifs = S.NotifService.list.filter(n => n.state !== "dismissed");
|
||||||
const groups = {};
|
const groups = {};
|
||||||
for (const n of notifs) {
|
for (const n of notifs) {
|
||||||
const key = n.appIcon || n.appName || "unknown";
|
const key = n.appIcon || n.appName || "unknown";
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ M.HoverPanel {
|
||||||
|
|
||||||
// DND toggle
|
// DND toggle
|
||||||
Text {
|
Text {
|
||||||
text: M.NotifService.dnd ? "\uDB82\uDE93" : "\uDB80\uDC9C"
|
text: S.NotifService.dnd ? "\uDB82\uDE93" : "\uDB80\uDC9C"
|
||||||
color: M.NotifService.dnd ? S.Theme.base09 : S.Theme.base04
|
color: S.NotifService.dnd ? S.Theme.base09 : S.Theme.base04
|
||||||
font.pixelSize: S.Theme.fontSize
|
font.pixelSize: S.Theme.fontSize
|
||||||
font.family: S.Theme.iconFontFamily
|
font.family: S.Theme.iconFontFamily
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
@ -42,7 +42,7 @@ M.HoverPanel {
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: M.NotifService.toggleDnd()
|
onClicked: S.NotifService.toggleDnd()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ M.HoverPanel {
|
||||||
font.pixelSize: S.Theme.fontSize
|
font.pixelSize: S.Theme.fontSize
|
||||||
font.family: S.Theme.iconFontFamily
|
font.family: S.Theme.iconFontFamily
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: M.NotifService.count > 0
|
visible: S.NotifService.count > 0
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: clearArea
|
id: clearArea
|
||||||
|
|
@ -88,7 +88,7 @@ M.HoverPanel {
|
||||||
// Group notifications by appName, sorted by max urgency desc then most recent time desc
|
// Group notifications by appName, sorted by max urgency desc then most recent time desc
|
||||||
readonly property var _groups: {
|
readonly property var _groups: {
|
||||||
const map = {};
|
const map = {};
|
||||||
for (const n of M.NotifService.list) {
|
for (const n of S.NotifService.list) {
|
||||||
const key = n.appName || "";
|
const key = n.appName || "";
|
||||||
if (!map[key])
|
if (!map[key])
|
||||||
map[key] = {
|
map[key] = {
|
||||||
|
|
@ -167,14 +167,14 @@ M.HoverPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
function _cascadeDismiss() {
|
function _cascadeDismiss() {
|
||||||
if (M.NotifService.list.length === 0)
|
if (S.NotifService.list.length === 0)
|
||||||
return;
|
return;
|
||||||
const ids = M.NotifService.list.map(n => n.id);
|
const ids = S.NotifService.list.map(n => n.id);
|
||||||
_startCascade(_getVisibleNotifDelegates(), ids);
|
_startCascade(_getVisibleNotifDelegates(), ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _cascadeGroupDismiss(appName) {
|
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)
|
if (ids.length === 0)
|
||||||
return;
|
return;
|
||||||
_startCascade(_getVisibleNotifDelegates(appName), ids);
|
_startCascade(_getVisibleNotifDelegates(appName), ids);
|
||||||
|
|
@ -184,7 +184,7 @@ M.HoverPanel {
|
||||||
const ids = _pendingDismissIds;
|
const ids = _pendingDismissIds;
|
||||||
_pendingDismissIds = [];
|
_pendingDismissIds = [];
|
||||||
for (const id of ids)
|
for (const id of ids)
|
||||||
M.NotifService.dismiss(id);
|
S.NotifService.dismiss(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
property Component _cascadeTimer: Component {
|
property Component _cascadeTimer: Component {
|
||||||
|
|
@ -467,7 +467,7 @@ M.HoverPanel {
|
||||||
ScriptAction {
|
ScriptAction {
|
||||||
script: {
|
script: {
|
||||||
if (notifDelegate._notif && !notifDelegate._skipDismiss)
|
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
|
// Empty state
|
||||||
Text {
|
Text {
|
||||||
visible: M.NotifService.count === 0
|
visible: S.NotifService.count === 0
|
||||||
width: menuWindow.contentWidth
|
width: menuWindow.contentWidth
|
||||||
height: 48
|
height: 48
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ PanelWindow {
|
||||||
|
|
||||||
required property var screen
|
required property var screen
|
||||||
|
|
||||||
visible: M.NotifService.popups.length > 0 && !S.NiriIpc.overviewOpen
|
visible: S.NotifService.popups.length > 0 && !S.NiriIpc.overviewOpen
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
|
@ -34,7 +34,7 @@ PanelWindow {
|
||||||
property var _knownIds: ({})
|
property var _knownIds: ({})
|
||||||
|
|
||||||
Repeater {
|
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 {
|
delegate: Item {
|
||||||
id: popupItem
|
id: popupItem
|
||||||
|
|
@ -74,7 +74,7 @@ PanelWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: M.NotifService
|
target: S.NotifService
|
||||||
function onPopupExpiring(notifId) {
|
function onPopupExpiring(notifId) {
|
||||||
if (notifId === popupItem.modelData.id)
|
if (notifId === popupItem.modelData.id)
|
||||||
popupItem.animateDismiss(false);
|
popupItem.animateDismiss(false);
|
||||||
|
|
@ -152,7 +152,7 @@ PanelWindow {
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
ScriptAction {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,28 +8,28 @@ M.BarSection {
|
||||||
id: root
|
id: root
|
||||||
spacing: S.Theme.moduleSpacing
|
spacing: S.Theme.moduleSpacing
|
||||||
tooltip: {
|
tooltip: {
|
||||||
const parts = [M.NotifService.count + " notification" + (M.NotifService.count !== 1 ? "s" : "")];
|
const parts = [S.NotifService.count + " notification" + (S.NotifService.count !== 1 ? "s" : "")];
|
||||||
if (M.NotifService.dnd)
|
if (S.NotifService.dnd)
|
||||||
parts.push("Do not disturb");
|
parts.push("Do not disturb");
|
||||||
return parts.join("\n");
|
return parts.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
required property var bar
|
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 {
|
M.BarIcon {
|
||||||
icon: {
|
icon: {
|
||||||
if (M.NotifService.dnd)
|
if (S.NotifService.dnd)
|
||||||
return M.NotifService.count > 0 ? "\uDB80\uDCA0" : "\uDB82\uDE93";
|
return S.NotifService.count > 0 ? "\uDB80\uDCA0" : "\uDB82\uDE93";
|
||||||
return M.NotifService.count > 0 ? "\uDB84\uDD6B" : "\uDB80\uDC9C";
|
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
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
M.BarLabel {
|
M.BarLabel {
|
||||||
id: countLabel
|
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
|
color: root.hasUrgent ? S.Theme.base08 : root.accentColor
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
|
@ -61,9 +61,9 @@ M.BarSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: M.NotifService
|
target: S.NotifService
|
||||||
function onCountChanged() {
|
function onCountChanged() {
|
||||||
if (M.NotifService.count > 0)
|
if (S.NotifService.count > 0)
|
||||||
popAnim.start();
|
popAnim.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +77,7 @@ M.BarSection {
|
||||||
}
|
}
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onTapped: M.NotifService.toggleDnd()
|
onTapped: S.NotifService.toggleDnd()
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyLoader {
|
LazyLoader {
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,6 @@ PowerProfileModule 1.0 PowerProfileModule.qml
|
||||||
IdleInhibitorModule 1.0 IdleInhibitorModule.qml
|
IdleInhibitorModule 1.0 IdleInhibitorModule.qml
|
||||||
NotificationsModule 1.0 NotificationsModule.qml
|
NotificationsModule 1.0 NotificationsModule.qml
|
||||||
ProcessList 1.0 ProcessList.qml
|
ProcessList 1.0 ProcessList.qml
|
||||||
singleton NotifService 1.0 NotifService.qml
|
|
||||||
NotifItem 1.0 NotifItem.qml
|
|
||||||
NotifPopup 1.0 NotifPopup.qml
|
NotifPopup 1.0 NotifPopup.qml
|
||||||
NotifCenter 1.0 NotifCenter.qml
|
NotifCenter 1.0 NotifCenter.qml
|
||||||
NotifCard 1.0 NotifCard.qml
|
NotifCard 1.0 NotifCard.qml
|
||||||
|
|
|
||||||
64
shell/services/NotifItem.qml
Normal file
64
shell/services/NotifItem.qml
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import "." 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 = S.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")
|
||||||
|
S.NotifService.dismiss(root.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginDismiss() {
|
||||||
|
if (state === "visible")
|
||||||
|
state = "dismissing";
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishDismiss() {
|
||||||
|
state = "dismissed";
|
||||||
|
_expireTimer.running = false;
|
||||||
|
notification?.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
137
shell/services/NotifService.qml
Normal file
137
shell/services/NotifService.qml
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import "." 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,3 +4,5 @@ singleton SystemStats 1.0 SystemStats.qml
|
||||||
singleton Modules 1.0 Modules.qml
|
singleton Modules 1.0 Modules.qml
|
||||||
singleton NiriIpc 1.0 NiriIpc.qml
|
singleton NiriIpc 1.0 NiriIpc.qml
|
||||||
singleton PowerProfileService 1.0 PowerProfileService.qml
|
singleton PowerProfileService 1.0 PowerProfileService.qml
|
||||||
|
singleton NotifService 1.0 NotifService.qml
|
||||||
|
NotifItem 1.0 NotifItem.qml
|
||||||
|
|
|
||||||
Reference in a new issue