C1: NotifItem QtObject with owned timer, refactor service to stable identities
This commit is contained in:
parent
45704cb102
commit
88d8842064
3 changed files with 136 additions and 52 deletions
67
modules/NotifItem.qml
Normal file
67
modules/NotifItem.qml
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import "." as M
|
||||||
|
|
||||||
|
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
|
||||||
|
property string timeStr: "now"
|
||||||
|
readonly property Timer _timeStrTimer: Timer {
|
||||||
|
running: root.state !== "dismissed"
|
||||||
|
repeat: true
|
||||||
|
interval: 5000
|
||||||
|
onTriggered: root._updateTimeStr()
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateTimeStr() {
|
||||||
|
const diff = Date.now() - time;
|
||||||
|
const m = Math.floor(diff / 60000);
|
||||||
|
if (m < 1) {
|
||||||
|
timeStr = "now";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const h = Math.floor(m / 60);
|
||||||
|
if (h < 1) {
|
||||||
|
timeStr = m + "m";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = Math.floor(h / 24);
|
||||||
|
timeStr = d > 0 ? d + "d" : h + "h";
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginDismiss() {
|
||||||
|
if (state === "visible")
|
||||||
|
state = "dismissing";
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishDismiss() {
|
||||||
|
state = "dismissed";
|
||||||
|
_expireTimer.running = false;
|
||||||
|
notification?.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,30 +12,37 @@ QtObject {
|
||||||
property var list: []
|
property var list: []
|
||||||
property bool dnd: false
|
property bool dnd: false
|
||||||
|
|
||||||
readonly property var popups: list.filter(n => n.popup)
|
readonly property var popups: list.filter(n => n.popup && n.state !== "dismissed")
|
||||||
readonly property int count: list.length
|
readonly property int count: list.filter(n => n.state !== "dismissed").length
|
||||||
|
|
||||||
|
// O(1) lookup
|
||||||
|
property var _byId: ({})
|
||||||
|
|
||||||
function dismiss(notifId) {
|
function dismiss(notifId) {
|
||||||
const idx = list.findIndex(n => n.id === notifId);
|
const item = _byId[notifId];
|
||||||
if (idx >= 0) {
|
if (!item)
|
||||||
const n = list[idx];
|
return;
|
||||||
n.notification?.dismiss();
|
item.finishDismiss();
|
||||||
list.splice(idx, 1);
|
list = list.filter(n => n !== item);
|
||||||
_changed();
|
delete _byId[notifId];
|
||||||
}
|
item.destroy();
|
||||||
|
_saveTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissAll() {
|
function dismissAll() {
|
||||||
for (const n of list.slice())
|
for (const item of list.slice()) {
|
||||||
n.notification?.dismiss();
|
item.finishDismiss();
|
||||||
|
delete _byId[item.id];
|
||||||
|
item.destroy();
|
||||||
|
}
|
||||||
list = [];
|
list = [];
|
||||||
_changed();
|
_saveTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissPopup(notifId) {
|
function dismissPopup(notifId) {
|
||||||
const n = list.find(n => n.id === notifId);
|
const item = _byId[notifId];
|
||||||
if (n) {
|
if (item) {
|
||||||
n.popup = false;
|
item.popup = false;
|
||||||
_changed();
|
_changed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,9 +53,11 @@ QtObject {
|
||||||
|
|
||||||
function _changed() {
|
function _changed() {
|
||||||
list = list.slice();
|
list = list.slice();
|
||||||
_saveTimer.restart();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Signal popups to animate out before removal
|
||||||
|
signal popupExpiring(var notifId)
|
||||||
|
|
||||||
property NotificationServer _server: NotificationServer {
|
property NotificationServer _server: NotificationServer {
|
||||||
actionsSupported: true
|
actionsSupported: true
|
||||||
bodyMarkupSupported: true
|
bodyMarkupSupported: true
|
||||||
|
|
@ -59,13 +68,17 @@ QtObject {
|
||||||
onNotification: notif => {
|
onNotification: notif => {
|
||||||
notif.tracked = true;
|
notif.tracked = true;
|
||||||
|
|
||||||
const data = {
|
const isCritical = notif.urgency === NotificationUrgency.Critical;
|
||||||
|
|
||||||
|
const item = _itemComp.createObject(root, {
|
||||||
|
notification: notif,
|
||||||
id: notif.id,
|
id: notif.id,
|
||||||
summary: notif.summary,
|
summary: notif.summary,
|
||||||
body: notif.body,
|
body: notif.body,
|
||||||
appName: notif.appName,
|
appName: notif.appName,
|
||||||
appIcon: notif.appIcon,
|
appIcon: notif.appIcon,
|
||||||
image: notif.image,
|
image: notif.image,
|
||||||
|
hints: notif.hints,
|
||||||
urgency: notif.urgency,
|
urgency: notif.urgency,
|
||||||
actions: notif.actions ? notif.actions.map(a => ({
|
actions: notif.actions ? notif.actions.map(a => ({
|
||||||
identifier: a.identifier,
|
identifier: a.identifier,
|
||||||
|
|
@ -73,14 +86,13 @@ QtObject {
|
||||||
invoke: () => a.invoke()
|
invoke: () => a.invoke()
|
||||||
})) : [],
|
})) : [],
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
popup: !root.dnd,
|
popup: isCritical || !root.dnd
|
||||||
closed: false,
|
});
|
||||||
notification: notif
|
|
||||||
};
|
|
||||||
|
|
||||||
root.list = [data, ...root.list];
|
root._byId[item.id] = item;
|
||||||
|
root.list = [item, ...root.list];
|
||||||
|
|
||||||
// Dismiss excess popups (keep in history, just hide popup)
|
// Trim excess popups
|
||||||
const max = M.Modules.notifications.maxPopups || 4;
|
const max = M.Modules.notifications.maxPopups || 4;
|
||||||
const currentPopups = root.list.filter(n => n.popup);
|
const currentPopups = root.list.filter(n => n.popup);
|
||||||
if (currentPopups.length > max) {
|
if (currentPopups.length > max) {
|
||||||
|
|
@ -89,38 +101,33 @@ QtObject {
|
||||||
root._changed();
|
root._changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-expire popup
|
// Auto-expire popup (skip for critical)
|
||||||
if (data.popup) {
|
if (item.popup && !isCritical) {
|
||||||
const timeout = notif.expireTimeout > 0 ? notif.expireTimeout : (M.Modules.notifications.timeout || 3000);
|
const timeout = notif.expireTimeout > 0 ? notif.expireTimeout : (M.Modules.notifications.timeout || 3000);
|
||||||
Qt.callLater(() => {
|
item._expireTimer.interval = timeout;
|
||||||
_expireTimer.createObject(root, {
|
item._expireTimer.running = true;
|
||||||
_notifId: data.id,
|
}
|
||||||
interval: timeout
|
|
||||||
});
|
// Trim history
|
||||||
});
|
const maxHistory = M.Modules.notifications.maxHistory || 50;
|
||||||
|
while (root.list.length > maxHistory) {
|
||||||
|
const old = root.list.pop();
|
||||||
|
old.finishDismiss();
|
||||||
|
delete root._byId[old.id];
|
||||||
|
old.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal popups to animate out before removal
|
property Component _itemComp: Component {
|
||||||
signal popupExpiring(var notifId)
|
NotifItem {}
|
||||||
|
|
||||||
property Component _expireTimer: Component {
|
|
||||||
Timer {
|
|
||||||
property var _notifId
|
|
||||||
running: true
|
|
||||||
onTriggered: {
|
|
||||||
root.popupExpiring(_notifId);
|
|
||||||
destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persistence
|
// Persistence
|
||||||
property Timer _saveTimer: Timer {
|
property Timer _saveTimer: Timer {
|
||||||
interval: 1000
|
interval: 1000
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
const data = root.list.map(n => ({
|
const data = root.list.filter(n => n.state !== "dismissed").map(n => ({
|
||||||
id: n.id,
|
id: n.id,
|
||||||
summary: n.summary,
|
summary: n.summary,
|
||||||
body: n.body,
|
body: n.body,
|
||||||
|
|
@ -139,16 +146,25 @@ QtObject {
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(text());
|
const data = JSON.parse(text());
|
||||||
for (let i = 0; i < data.length; i++) {
|
const maxHistory = M.Modules.notifications.maxHistory || 50;
|
||||||
|
for (let i = 0; i < Math.min(data.length, maxHistory); i++) {
|
||||||
const n = data[i];
|
const n = data[i];
|
||||||
// Prefix persisted IDs to avoid collision with live D-Bus IDs
|
const item = _itemComp.createObject(root, {
|
||||||
n.id = "p_" + (n.id ?? i) + "_" + n.time;
|
id: "p_" + (n.id ?? i) + "_" + n.time,
|
||||||
n.popup = false;
|
summary: n.summary || "",
|
||||||
n.closed = false;
|
body: n.body || "",
|
||||||
n.notification = null;
|
appName: n.appName || "",
|
||||||
n.actions = [];
|
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.list = data.concat(root.list);
|
root._changed();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ PowerProfile 1.0 PowerProfile.qml
|
||||||
IdleInhibitor 1.0 IdleInhibitor.qml
|
IdleInhibitor 1.0 IdleInhibitor.qml
|
||||||
Notifications 1.0 Notifications.qml
|
Notifications 1.0 Notifications.qml
|
||||||
singleton NotifService 1.0 NotifService.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
|
||||||
Power 1.0 Power.qml
|
Power 1.0 Power.qml
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue