nova-shell/modules/NotifService.qml

181 lines
5.7 KiB
QML

pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Notifications
import "." as M
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();
_saveTimer.restart();
}
function dismissAll() {
for (const item of list.slice()) {
item.finishDismiss();
delete _byId[item.id];
item.destroy();
}
list = [];
_saveTimer.restart();
}
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];
// Trim excess popups
const max = M.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 : (M.Modules.notifications.timeout || 3000);
item._expireTimer.interval = timeout;
item._expireTimer.running = true;
}
// Trim history (-1 = unlimited)
const maxHistory = M.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 {}
}
// Persistence
// 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()
}
property Timer _saveTimer: Timer {
interval: 1000
onTriggered: {
const data = root.list.filter(n => n.state !== "dismissed").map(n => ({
id: n.id,
summary: n.summary,
body: n.body,
appName: n.appName,
appIcon: n.appIcon,
image: n.image,
urgency: n.urgency,
time: n.time
}));
_storage.setText(JSON.stringify(data));
}
}
property FileView _storage: FileView {
path: (Quickshell.env("XDG_STATE_HOME") || (Quickshell.env("HOME") + "/.local/state")) + "/nova-shell/notifs.json"
onLoaded: {
try {
const data = JSON.parse(text());
const maxHistory = M.Modules.notifications.maxHistory ?? -1;
const limit = maxHistory > 0 ? Math.min(data.length, maxHistory) : data.length;
for (let i = 0; i < limit; i++) {
const n = data[i];
const item = _itemComp.createObject(root, {
id: "p_" + (n.id ?? i) + "_" + n.time,
summary: n.summary || "",
body: n.body || "",
appName: n.appName || "",
appIcon: n.appIcon || "",
image: n.image || "",
urgency: n.urgency ?? 1,
time: n.time || Date.now(),
popup: false,
actions: []
});
root._byId[item.id] = item;
root.list.push(item);
}
root._changed();
} catch (e) {}
}
}
}