347 lines
9.9 KiB
QML
347 lines
9.9 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Notifications
|
|
import Quickshell.Hyprland
|
|
import QtQuick
|
|
import ZShell
|
|
import qs.Modules
|
|
import qs.Helpers
|
|
import qs.Paths
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
property list<Notif> list: []
|
|
readonly property list<Notif> notClosed: list.filter( n => !n.closed )
|
|
readonly property list<Notif> popups: list.filter( n => n.popup )
|
|
property alias dnd: props.dnd
|
|
property alias server: server
|
|
|
|
property bool loaded
|
|
|
|
onListChanged: {
|
|
if ( loaded ) {
|
|
saveTimer.restart();
|
|
}
|
|
if ( root.list.length > 0 ) {
|
|
HasNotifications.hasNotifications = true;
|
|
} else {
|
|
HasNotifications.hasNotifications = false;
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: saveTimer
|
|
interval: 1000
|
|
onTriggered: storage.setText( JSON.stringify( root.notClosed.map( n => ({
|
|
time: n.time,
|
|
id: n.id,
|
|
summary: n.summary,
|
|
body: n.body,
|
|
appIcon: n.appIcon,
|
|
appName: n.appName,
|
|
image: n.image,
|
|
expireTimeout: n.expireTimeout,
|
|
urgency: n.urgency,
|
|
resident: n.resident,
|
|
hasActionIcons: n.hasActionIcons,
|
|
actions: n.actions
|
|
}))));
|
|
}
|
|
|
|
PersistentProperties {
|
|
id: props
|
|
|
|
property bool dnd
|
|
|
|
reloadableId: "notifs"
|
|
}
|
|
|
|
NotificationServer {
|
|
id: server
|
|
|
|
keepOnReload: false
|
|
actionsSupported: true
|
|
bodyHyperlinksSupported: true
|
|
bodyImagesSupported: true
|
|
bodyMarkupSupported: true
|
|
imageSupported: true
|
|
persistenceSupported: true
|
|
|
|
onNotification: notif => {
|
|
notif.tracked = true;
|
|
|
|
const comp = notifComp.createObject(root, {
|
|
popup: !props.dnd,
|
|
notification: notif
|
|
});
|
|
root.list = [comp, ...root.list];
|
|
}
|
|
}
|
|
|
|
FileView {
|
|
id: storage
|
|
path: `${Paths.state}/notifs.json`
|
|
|
|
onLoaded: {
|
|
const data = JSON.parse(text());
|
|
for (const notif of data)
|
|
root.list.push(notifComp.createObject(root, notif));
|
|
root.list.sort((a, b) => b.time - a.time);
|
|
root.loaded = true;
|
|
}
|
|
|
|
onLoadFailed: err => {
|
|
if (err === FileViewError.FileNotFound) {
|
|
root.loaded = true;
|
|
setText("[]");
|
|
}
|
|
}
|
|
}
|
|
|
|
GlobalShortcut {
|
|
name: "clearNotifs"
|
|
description: "Clear all notifications"
|
|
onPressed: {
|
|
for (const notif of root.list.slice())
|
|
notif.close();
|
|
}
|
|
}
|
|
|
|
IpcHandler {
|
|
target: "notifs"
|
|
|
|
function clear(): void {
|
|
for (const notif of root.list.slice())
|
|
notif.close();
|
|
}
|
|
|
|
function isDndEnabled(): bool {
|
|
return props.dnd;
|
|
}
|
|
|
|
function toggleDnd(): void {
|
|
props.dnd = !props.dnd;
|
|
}
|
|
|
|
function enableDnd(): void {
|
|
props.dnd = true;
|
|
}
|
|
|
|
function disableDnd(): void {
|
|
props.dnd = false;
|
|
}
|
|
}
|
|
|
|
component Notif: QtObject {
|
|
id: notif
|
|
|
|
property bool popup
|
|
property bool closed
|
|
property var locks: new Set()
|
|
|
|
property date time: new Date()
|
|
readonly property string timeStr: {
|
|
const diff = Time.date.getTime() - time.getTime();
|
|
const m = Math.floor(diff / 60000);
|
|
|
|
if (m < 1)
|
|
return qsTr("now");
|
|
|
|
const h = Math.floor(m / 60);
|
|
const d = Math.floor(h / 24);
|
|
|
|
if (d > 0)
|
|
return `${d}d`;
|
|
if (h > 0)
|
|
return `${h}h`;
|
|
return `${m}m`;
|
|
}
|
|
|
|
property Notification notification
|
|
property string id
|
|
property string summary
|
|
property string body
|
|
property string appIcon
|
|
property string appName
|
|
property string image
|
|
property real expireTimeout: 5
|
|
property int urgency: NotificationUrgency.Normal
|
|
property bool resident
|
|
property bool hasActionIcons
|
|
property list<var> actions
|
|
|
|
readonly property Timer timer: Timer {
|
|
property int totalTime: 5000
|
|
property int remainingTime: totalTime
|
|
property bool paused: false
|
|
|
|
running: !paused
|
|
repeat: true
|
|
interval: 50
|
|
onTriggered: {
|
|
remainingTime -= interval;
|
|
|
|
if ( remainingTime <= 0 ) {
|
|
remainingTime = 0;
|
|
notif.popup = false;
|
|
stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
readonly property LazyLoader dummyImageLoader: LazyLoader {
|
|
active: false
|
|
|
|
PanelWindow {
|
|
implicitWidth: Config.notifs.sizes.image
|
|
implicitHeight: Config.notifs.sizes.image
|
|
color: "transparent"
|
|
mask: Region {}
|
|
|
|
Image {
|
|
function tryCache(): void {
|
|
if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image)
|
|
return;
|
|
|
|
const cacheKey = notif.appName + notif.summary + notif.id;
|
|
let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch;
|
|
for (let i = 0; i < cacheKey.length; i++) {
|
|
ch = cacheKey.charCodeAt(i);
|
|
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
}
|
|
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
|
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
|
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0);
|
|
|
|
const cache = `${Paths.notifimagecache}/${hash}.png`;
|
|
ZShell.saveItem(this, Qt.resolvedUrl(cache), () => {
|
|
notif.image = cache;
|
|
notif.dummyImageLoader.active = false;
|
|
});
|
|
}
|
|
|
|
anchors.fill: parent
|
|
source: Qt.resolvedUrl(notif.image)
|
|
fillMode: Image.PreserveAspectCrop
|
|
cache: false
|
|
asynchronous: true
|
|
opacity: 0
|
|
|
|
onStatusChanged: tryCache()
|
|
onWidthChanged: tryCache()
|
|
onHeightChanged: tryCache()
|
|
}
|
|
}
|
|
}
|
|
|
|
readonly property Connections conn: Connections {
|
|
target: notif.notification
|
|
|
|
function onClosed(): void {
|
|
notif.close();
|
|
}
|
|
|
|
function onSummaryChanged(): void {
|
|
notif.summary = notif.notification.summary;
|
|
}
|
|
|
|
function onBodyChanged(): void {
|
|
notif.body = notif.notification.body;
|
|
}
|
|
|
|
function onAppIconChanged(): void {
|
|
notif.appIcon = notif.notification.appIcon;
|
|
}
|
|
|
|
function onAppNameChanged(): void {
|
|
notif.appName = notif.notification.appName;
|
|
}
|
|
|
|
function onImageChanged(): void {
|
|
notif.image = notif.notification.image;
|
|
if (notif.notification?.image)
|
|
notif.dummyImageLoader.active = true;
|
|
}
|
|
|
|
function onExpireTimeoutChanged(): void {
|
|
notif.expireTimeout = notif.notification.expireTimeout;
|
|
}
|
|
|
|
function onUrgencyChanged(): void {
|
|
notif.urgency = notif.notification.urgency;
|
|
}
|
|
|
|
function onResidentChanged(): void {
|
|
notif.resident = notif.notification.resident;
|
|
}
|
|
|
|
function onHasActionIconsChanged(): void {
|
|
notif.hasActionIcons = notif.notification.hasActionIcons;
|
|
}
|
|
|
|
function onActionsChanged(): void {
|
|
notif.actions = notif.notification.actions.map(a => ({
|
|
identifier: a.identifier,
|
|
text: a.text,
|
|
invoke: () => a.invoke()
|
|
}));
|
|
}
|
|
}
|
|
|
|
function lock(item: Item): void {
|
|
locks.add(item);
|
|
}
|
|
|
|
function unlock(item: Item): void {
|
|
locks.delete(item);
|
|
if (closed)
|
|
close();
|
|
}
|
|
|
|
function close(): void {
|
|
closed = true;
|
|
if (locks.size === 0 && root.list.includes(this)) {
|
|
root.list = root.list.filter(n => n !== this);
|
|
notification?.dismiss();
|
|
destroy();
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
if (!notification)
|
|
return;
|
|
|
|
id = notification.id;
|
|
summary = notification.summary;
|
|
body = notification.body;
|
|
appIcon = notification.appIcon;
|
|
appName = notification.appName;
|
|
image = notification.image;
|
|
if (notification?.image)
|
|
dummyImageLoader.active = true;
|
|
expireTimeout = notification.expireTimeout;
|
|
urgency = notification.urgency;
|
|
resident = notification.resident;
|
|
hasActionIcons = notification.hasActionIcons;
|
|
actions = notification.actions.map(a => ({
|
|
identifier: a.identifier,
|
|
text: a.text,
|
|
invoke: () => a.invoke()
|
|
}));
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: notifComp
|
|
|
|
Notif {}
|
|
}
|
|
}
|