172 lines
5.4 KiB
QML
172 lines
5.4 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
|
|
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) {}
|
|
}
|
|
}
|
|
}
|