**animations** #5
@@ -17,6 +17,10 @@ Scope {
|
|||||||
screen: modelData
|
screen: modelData
|
||||||
property var root: Quickshell.shellDir
|
property var root: Quickshell.shellDir
|
||||||
|
|
||||||
|
NotificationCenter {
|
||||||
|
bar: bar
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: ncProcess
|
id: ncProcess
|
||||||
command: ["sh", "-c", `qs -p ${bar.root}/shell.qml ipc call root showCenter`]
|
command: ["sh", "-c", `qs -p ${bar.root}/shell.qml ipc call root showCenter`]
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import QtQuick
|
||||||
|
import qs.Modules
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
|
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: NotifPath.notifPath
|
||||||
|
|
||||||
|
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("[]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
running: true
|
||||||
|
interval: 5000
|
||||||
|
onTriggered: {
|
||||||
|
notif.popup = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias centerX: notifCenterSpacing.centerX
|
||||||
|
|
||||||
|
JsonAdapter {
|
||||||
|
id: notifCenterSpacing
|
||||||
|
|
||||||
|
property int centerX
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias notifPath: storage.notifPath
|
||||||
|
|
||||||
|
JsonAdapter {
|
||||||
|
id: storage
|
||||||
|
|
||||||
|
property string notifPath: Quickshell.statePath("notifications.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
property alias enabled: clock.enabled
|
||||||
|
readonly property date date: clock.date
|
||||||
|
readonly property int hours: clock.hours
|
||||||
|
readonly property int minutes: clock.minutes
|
||||||
|
readonly property int seconds: clock.seconds
|
||||||
|
|
||||||
|
function format(fmt: string): string {
|
||||||
|
return Qt.formatDateTime(clock.date, fmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemClock {
|
||||||
|
id: clock
|
||||||
|
precision: SystemClock.Seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Notifications
|
|
||||||
import QtQuick
|
|
||||||
import qs.Modules
|
|
||||||
|
|
||||||
Scope {
|
|
||||||
id: root
|
|
||||||
property list<int> notifIds: []
|
|
||||||
property list<TrackedNotification> notifications;
|
|
||||||
NotificationServer {
|
|
||||||
id: notificationServer
|
|
||||||
imageSupported: true
|
|
||||||
actionsSupported: true
|
|
||||||
persistenceSupported: true
|
|
||||||
bodyImagesSupported: true
|
|
||||||
bodySupported: true
|
|
||||||
onNotification: notification => {
|
|
||||||
notification.tracked = true;
|
|
||||||
notification.receivedTime = Date.now();
|
|
||||||
root.notifIds.push(notification.id);
|
|
||||||
const notif = notificationComponent.createObject(root, { notif: notification, visible: !notificationCenter.doNotDisturb });
|
|
||||||
root.notifications.push(notif);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: notificationComponent
|
|
||||||
TrackedNotification {
|
|
||||||
centerX: notificationCenter.posX
|
|
||||||
notifIndex: root.notifIds
|
|
||||||
notifList: root.notifications
|
|
||||||
onNotifDestroy: {
|
|
||||||
root.notifications.shift();
|
|
||||||
root.notifIds.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationCenter {
|
|
||||||
id: notificationCenter
|
|
||||||
notifications: notificationServer.trackedNotifications.values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import QtQuick
|
||||||
|
import qs.Modules
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
property list<int> notifIds: []
|
||||||
|
property list<TrackedNotification> notifications;
|
||||||
|
// NotificationServer {
|
||||||
|
// id: notificationServer
|
||||||
|
// imageSupported: true
|
||||||
|
// actionsSupported: true
|
||||||
|
// persistenceSupported: true
|
||||||
|
// bodyImagesSupported: true
|
||||||
|
// bodySupported: true
|
||||||
|
// onNotification: notification => {
|
||||||
|
// notification.tracked = true;
|
||||||
|
// notification.receivedTime = Date.now();
|
||||||
|
// root.notifIds.push(notification.id);
|
||||||
|
// const notif = notificationComponent.createObject(root, { notif: notification, visible: !notificationCenter.doNotDisturb });
|
||||||
|
// root.notifications.push(notif);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: NotifServer.server
|
||||||
|
function onNotification() {
|
||||||
|
notificationComponent.createObject( root, { notif: NotifServer.list[0] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: notificationComponent
|
||||||
|
TrackedNotification {
|
||||||
|
centerX: notificationCenter.posX
|
||||||
|
notifIndex: root.notifIds
|
||||||
|
notifList: root.notifications
|
||||||
|
onNotifDestroy: {
|
||||||
|
root.notifications.shift();
|
||||||
|
root.notifIds.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter {
|
||||||
|
id: notificationCenter
|
||||||
|
notifications: notificationServer.trackedNotifications.values
|
||||||
|
}
|
||||||
|
}
|
||||||
+145
-87
@@ -2,6 +2,7 @@ import Quickshell
|
|||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls.FluentWinUI3
|
import QtQuick.Controls.FluentWinUI3
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
@@ -9,6 +10,7 @@ import QtQuick
|
|||||||
import Quickshell.Services.Notifications
|
import Quickshell.Services.Notifications
|
||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
|
import qs.Daemons
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: root
|
id: root
|
||||||
@@ -19,22 +21,15 @@ PanelWindow {
|
|||||||
left: true
|
left: true
|
||||||
bottom: true
|
bottom: true
|
||||||
}
|
}
|
||||||
required property list<Notification> notifications
|
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
required property PanelWindow bar
|
||||||
property bool centerShown: false
|
property bool centerShown: false
|
||||||
property alias posX: backgroundRect.x
|
property alias posX: backgroundRect.x
|
||||||
property alias doNotDisturb: dndSwitch.checked
|
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
mask: Region { item: backgroundRect }
|
mask: Region { item: backgroundRect }
|
||||||
|
|
||||||
onNotificationsChanged: {
|
|
||||||
if ( root.notifications.length > 0 ) {
|
|
||||||
HasNotifications.hasNotifications = true;
|
|
||||||
} else {
|
|
||||||
HasNotifications.hasNotifications = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
id: ipcHandler
|
id: ipcHandler
|
||||||
target: "root"
|
target: "root"
|
||||||
@@ -69,10 +64,10 @@ PanelWindow {
|
|||||||
id: showAnimation
|
id: showAnimation
|
||||||
target: backgroundRect
|
target: backgroundRect
|
||||||
property: "x"
|
property: "x"
|
||||||
from: Screen.width
|
to: root.bar.screen.width - backgroundRect.implicitWidth - 10
|
||||||
to: Screen.width - backgroundRect.implicitWidth - 10
|
from: root.bar.screen.width
|
||||||
duration: 300
|
duration: MaterialEasing.expressiveEffectsTime
|
||||||
easing.type: Easing.OutCubic
|
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||||
onStopped: {
|
onStopped: {
|
||||||
focusGrab.active = true;
|
focusGrab.active = true;
|
||||||
}
|
}
|
||||||
@@ -82,42 +77,10 @@ PanelWindow {
|
|||||||
id: closeAnimation
|
id: closeAnimation
|
||||||
target: backgroundRect
|
target: backgroundRect
|
||||||
property: "x"
|
property: "x"
|
||||||
from: Screen.width - backgroundRect.implicitWidth - 10
|
from: root.bar.screen.width - backgroundRect.implicitWidth - 10
|
||||||
to: Screen.width
|
to: root.bar.screen.width
|
||||||
duration: 300
|
duration: MaterialEasing.expressiveEffectsTime
|
||||||
easing.type: Easing.OutCubic
|
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||||
}
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: groupedData
|
|
||||||
property var groups: ({})
|
|
||||||
|
|
||||||
function updateGroups() {
|
|
||||||
var newGroups = {};
|
|
||||||
for ( var i = 0; i < root.notifications.length; i++ ) {
|
|
||||||
var notif = root.notifications[ i ];
|
|
||||||
var appName = notif.appName || "Unknown";
|
|
||||||
if ( !newGroups[ appName ]) {
|
|
||||||
newGroups[ appName ] = [];
|
|
||||||
}
|
|
||||||
newGroups[ appName ].push( notif );
|
|
||||||
}
|
|
||||||
// Sort notifications within each group (latest first)
|
|
||||||
for ( var app in newGroups ) {
|
|
||||||
newGroups[ app ].sort(( a, b ) => b.receivedTime - a.receivedTime );
|
|
||||||
}
|
|
||||||
groups = newGroups;
|
|
||||||
groupsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: updateGroups()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onNotificationsChanged() {
|
|
||||||
groupedData.updateGroups();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HyprlandFocusGrab {
|
HyprlandFocusGrab {
|
||||||
@@ -129,10 +92,16 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TrackedNotification {
|
||||||
|
centerShown: root.centerShown
|
||||||
|
bar: root.bar
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: backgroundRect
|
id: backgroundRect
|
||||||
y: 10
|
y: 10
|
||||||
x: Screen.width
|
x: Screen.width
|
||||||
|
z: 1
|
||||||
implicitWidth: 400
|
implicitWidth: 400
|
||||||
implicitHeight: root.height - 20
|
implicitHeight: root.height - 20
|
||||||
color: Config.baseBgColor
|
color: Config.baseBgColor
|
||||||
@@ -154,6 +123,9 @@ PanelWindow {
|
|||||||
focus: false
|
focus: false
|
||||||
activeFocusOnTab: false
|
activeFocusOnTab: false
|
||||||
focusPolicy: Qt.NoFocus
|
focusPolicy: Qt.NoFocus
|
||||||
|
onCheckedChanged: {
|
||||||
|
NotifServer.dnd = dndSwitch.checked;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||||
@@ -180,9 +152,8 @@ PanelWindow {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
for ( var app in groupedData.groups ) {
|
for ( const n of NotifServer.notClosed )
|
||||||
groupedData.groups[ app ].forEach( function( n ) { n.dismiss(); });
|
n.close();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,6 +169,7 @@ PanelWindow {
|
|||||||
Flickable {
|
Flickable {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
pixelAligned: true
|
||||||
contentHeight: notificationColumn.implicitHeight
|
contentHeight: notificationColumn.implicitHeight
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
@@ -216,28 +188,52 @@ PanelWindow {
|
|||||||
|
|
||||||
move: Transition {
|
move: Transition {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
properties: "y,x";
|
properties: "x";
|
||||||
duration: 200;
|
duration: 200;
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Object.keys( groupedData.groups )
|
model: ScriptModel {
|
||||||
|
values: {
|
||||||
|
const map = new Map();
|
||||||
|
for ( const n of NotifServer.notClosed )
|
||||||
|
map.set( n.appName, null );
|
||||||
|
for ( const n of NotifServer.list )
|
||||||
|
map.set( n.appName, null );
|
||||||
|
return [ ...map.keys() ];
|
||||||
|
}
|
||||||
|
onValuesChanged: {
|
||||||
|
root.flagChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
Column {
|
Column {
|
||||||
id: groupColumn
|
id: groupColumn
|
||||||
required property string modelData
|
required property string modelData
|
||||||
property var notifications: groupedData.groups[ modelData ]
|
property list<var> notifications: NotifServer.list.filter( n => n.appName === modelData )
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
property bool shouldShow: false
|
property bool shouldShow: false
|
||||||
property bool isExpanded: false
|
property bool isExpanded: false
|
||||||
|
|
||||||
|
function closeAll(): void {
|
||||||
|
for ( const n of NotifServer.notClosed.filter( n => n.appName === modelData ))
|
||||||
|
n.close();
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on y {
|
||||||
|
Anim {
|
||||||
|
duration: MaterialEasing.expressiveEffectsTime
|
||||||
|
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
add: Transition {
|
add: Transition {
|
||||||
id: addTrans
|
id: addTrans
|
||||||
SequentialAnimation {
|
SequentialAnimation {
|
||||||
@@ -259,6 +255,13 @@ PanelWindow {
|
|||||||
duration: 100;
|
duration: 100;
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
properties: "scale";
|
||||||
|
from: 0.7;
|
||||||
|
to: 1.0;
|
||||||
|
duration: 100
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,9 +269,8 @@ PanelWindow {
|
|||||||
move: Transition {
|
move: Transition {
|
||||||
id: moveTrans
|
id: moveTrans
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
properties: "opacity";
|
properties: "y";
|
||||||
duration: 100;
|
duration: 100;
|
||||||
to: 0;
|
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.OutCubic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,46 +307,38 @@ PanelWindow {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
groupColumn.isExpanded = false;
|
groupColumn.shouldShow = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: groupColumn.notifications
|
id: groupListView
|
||||||
|
model: ScriptModel {
|
||||||
|
id: groupModel
|
||||||
|
values: groupColumn.isExpanded ? groupColumn.notifications : groupColumn.notifications.slice( 0, 1 )
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: groupHeader
|
id: groupHeader
|
||||||
required property var modelData
|
required property int index
|
||||||
required property var index
|
required property NotifServer.Notif modelData
|
||||||
|
property alias notifHeight: groupHeader.height
|
||||||
|
|
||||||
|
property bool previewHidden: groupColumn.shouldShow && index > 0
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: contentColumn.height + 20
|
height: contentColumn.height + 20
|
||||||
color: Config.baseBgColor
|
color: Config.baseBgColor
|
||||||
border.color: "#555555"
|
border.color: "#555555"
|
||||||
border.width: 1
|
border.width: 1
|
||||||
radius: 8
|
radius: 8
|
||||||
visible: groupColumn.notifications[0].id === modelData.id || groupColumn.isExpanded
|
opacity: previewHidden ? 0 : 1.0
|
||||||
opacity: groupColumn.notifications[0].id === modelData.id ? 1 : 0
|
scale: previewHidden ? 0.7 : 1.0
|
||||||
|
|
||||||
Connections {
|
Component.onCompleted: modelData.lock(this);
|
||||||
target: groupColumn
|
Component.onDestruction: modelData.unlock(this);
|
||||||
function onShouldShowChanged() {
|
|
||||||
if ( !shouldShow ) {
|
|
||||||
// collapseAnim.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if ( visible ) {
|
|
||||||
// expandAnim.start();
|
|
||||||
} else {
|
|
||||||
if ( groupColumn.notifications[0].id !== modelData.id ) {
|
|
||||||
groupHeader.opacity = 0;
|
|
||||||
}
|
|
||||||
groupColumn.isExpanded = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -354,11 +348,73 @@ PanelWindow {
|
|||||||
groupHeader.modelData.actions[0].invoke();
|
groupHeader.modelData.actions[0].invoke();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
groupColumn.shouldShow = true;
|
||||||
groupColumn.isExpanded = true;
|
groupColumn.isExpanded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: collapseAnim
|
||||||
|
running: !groupColumn.shouldShow && index > 0
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
target: groupHeader
|
||||||
|
property: "opacity"
|
||||||
|
duration: 100
|
||||||
|
from: 1
|
||||||
|
to: index > 0 ? 0 : 1.0
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: groupHeader
|
||||||
|
property: "scale"
|
||||||
|
duration: 100
|
||||||
|
from: 1
|
||||||
|
to: index > 0 ? 0.7 : 1.0
|
||||||
|
}
|
||||||
|
onFinished: {
|
||||||
|
groupColumn.isExpanded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
running: groupHeader.modelData.closed
|
||||||
|
onFinished: groupHeader.modelData.unlock(groupHeader)
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
target: groupHeader
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: groupHeader
|
||||||
|
property: "x"
|
||||||
|
to: groupHeader.x >= 0 ? groupHeader.width : -groupHeader.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
Anim {
|
||||||
|
duration: MaterialEasing.expressiveDefaultSpatialTime
|
||||||
|
easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on y {
|
||||||
|
Anim {
|
||||||
|
duration: MaterialEasing.expressiveDefaultSpatialTime
|
||||||
|
easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: contentColumn
|
id: contentColumn
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -420,13 +476,15 @@ PanelWindow {
|
|||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 2
|
spacing: 2
|
||||||
visible: groupColumn.isExpanded ? ( groupHeader.modelData.actions.length > 1 ? true : false ) : ( groupColumn.notifications.length === 1 ? ( groupHeader.modelData.actions.length > 1 ? true : false ) : false )
|
// visible: groupColumn.isExpanded ? ( groupHeader.modelData.actions.length > 1 ? true : false ) : ( groupColumn.notifications.length === 1 ? ( groupHeader.modelData.actions.length > 1 ? true : false ) : false )
|
||||||
|
visible: true
|
||||||
height: 30
|
height: 30
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: groupHeader.modelData.actions
|
model: groupHeader.modelData.actions
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: actionButton
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 30
|
Layout.preferredHeight: 30
|
||||||
required property var modelData
|
required property var modelData
|
||||||
@@ -434,7 +492,7 @@ PanelWindow {
|
|||||||
radius: 4
|
radius: 4
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: modelData.text
|
text: actionButton.modelData.text
|
||||||
color: "white"
|
color: "white"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
}
|
}
|
||||||
@@ -443,7 +501,7 @@ PanelWindow {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
modelData.invoke();
|
actionButton.modelData.invoke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,7 +530,7 @@ PanelWindow {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
groupColumn.isExpanded ? groupColumn.notifications[0].dismiss() : groupColumn.notifications.forEach( function( n ) { n.dismiss(); });
|
groupColumn.isExpanded ? groupHeader.modelData.close() : groupColumn.closeAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,122 @@
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Hyprland
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Services.Notifications
|
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
import qs.Daemons
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: root
|
id: root
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
screen: root.bar.screen
|
||||||
anchors {
|
anchors {
|
||||||
top: true
|
top: true
|
||||||
right: true
|
right: true
|
||||||
left: true
|
left: true
|
||||||
bottom: true
|
bottom: true
|
||||||
}
|
}
|
||||||
mask: Region { item: backgroundRect }
|
mask: Region { regions: root.notifRegions }
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
required property Notification notif
|
property list<Region> notifRegions: []
|
||||||
required property int centerX
|
required property bool centerShown
|
||||||
required property list<int> notifIndex
|
required property PanelWindow bar
|
||||||
required property list<TrackedNotification> notifList
|
visible: Hyprland.monitorFor(screen).focused
|
||||||
property int index: notifIndex.indexOf(notif.id)
|
|
||||||
property alias y: backgroundRect.y
|
|
||||||
property alias notifHeight: backgroundRect.implicitHeight
|
|
||||||
signal notifDestroy()
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
openAnim.start();
|
console.log(NotifServer.list.filter( n => n.popup ).length + " notification popups loaded.");
|
||||||
console.log(root.index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
ListView {
|
||||||
id: timeout
|
id: notifListView
|
||||||
interval: 5000
|
model: ScriptModel {
|
||||||
onTriggered: {
|
values: NotifServer.list.filter( n => n.popup )
|
||||||
closeAnim.start();
|
|
||||||
}
|
}
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
x: root.centerShown ? root.bar.width - width - 420 : root.bar.width - width - 20
|
||||||
|
z: 0
|
||||||
|
anchors.topMargin: 54
|
||||||
|
width: 400
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: MaterialEasing.expressiveEffectsTime
|
||||||
|
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displaced: Transition {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "y"
|
||||||
|
duration: 100
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove: Transition {
|
||||||
|
id: hideTransition
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "opacity"
|
||||||
|
from: 1
|
||||||
|
to: 0
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
}
|
}
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
id: openAnim
|
|
||||||
target: backgroundRect
|
|
||||||
property: "x"
|
property: "x"
|
||||||
from: root.centerX
|
to: hideTransition.ViewTransition.destination.x + 200
|
||||||
to: root.centerX - backgroundRect.implicitWidth - 20
|
duration: 200
|
||||||
|
easing.type: Easing.InOutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add: Transition {
|
||||||
|
id: showTransition
|
||||||
|
ParallelAnimation {
|
||||||
|
NumberAnimation {
|
||||||
|
property: "opacity"
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
duration: 200
|
duration: 200
|
||||||
easing.type: Easing.InOutQuad
|
easing.type: Easing.InOutQuad
|
||||||
onStopped: { timeout.start(); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
id: closeAnim
|
|
||||||
target: backgroundRect
|
|
||||||
property: "x"
|
property: "x"
|
||||||
from: root.centerX - backgroundRect.implicitWidth - 20
|
from: showTransition.ViewTransition.destination.x + 200
|
||||||
to: root.centerX
|
to: showTransition.ViewTransition.destination.x
|
||||||
duration: 200
|
duration: 200
|
||||||
easing.type: Easing.InOutQuad
|
easing.type: Easing.InOutQuad
|
||||||
onStopped: {
|
}
|
||||||
root.destroy();
|
|
||||||
root.notifDestroy();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
component NotifRegion: Region { }
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: notifRegion
|
||||||
|
NotifRegion {}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
id: backgroundRect
|
id: backgroundRect
|
||||||
|
required property NotifServer.Notif modelData
|
||||||
implicitWidth: 400
|
implicitWidth: 400
|
||||||
implicitHeight: contentLayout.childrenRect.height + 16
|
implicitHeight: contentLayout.childrenRect.height + 16
|
||||||
x: root.centerX - implicitWidth - 20
|
|
||||||
y: !root.notifList[ root.index - 1 ] ? 34 + 20 : root.notifList[ root.index - 1 ].y + root.notifList[ root.index - 1 ].notifHeight + 10
|
|
||||||
color: Config.baseBgColor
|
color: Config.baseBgColor
|
||||||
border.color: "#555555"
|
border.color: "#555555"
|
||||||
radius: 8
|
radius: 8
|
||||||
|
|
||||||
Behavior on y {
|
Component.onCompleted: {
|
||||||
NumberAnimation {
|
root.notifRegions.push( notifRegion.createObject(root, { item: backgroundRect }));
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.InOutQuad
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -92,11 +129,11 @@ PanelWindow {
|
|||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
IconImage {
|
IconImage {
|
||||||
source: root.notif.image
|
source: backgroundRect.modelData.image
|
||||||
Layout.preferredWidth: 48
|
Layout.preferredWidth: 48
|
||||||
Layout.preferredHeight: 48
|
Layout.preferredHeight: 48
|
||||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignLeft
|
Layout.alignment: Qt.AlignHCenter | Qt.AlignLeft
|
||||||
visible: root.notif.image !== ""
|
visible: backgroundRect.modelData.image !== ""
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
@@ -106,7 +143,7 @@ PanelWindow {
|
|||||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: root.notif.appName
|
text: backgroundRect.modelData.appName
|
||||||
color: "white"
|
color: "white"
|
||||||
font.bold: true
|
font.bold: true
|
||||||
font.pointSize: 14
|
font.pointSize: 14
|
||||||
@@ -116,7 +153,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: root.notif.summary
|
text: backgroundRect.modelData.summary
|
||||||
color: "white"
|
color: "white"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
font.bold: true
|
font.bold: true
|
||||||
@@ -128,7 +165,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
text: root.notif.body
|
text: backgroundRect.modelData.body
|
||||||
color: "#dddddd"
|
color: "#dddddd"
|
||||||
font.pointSize: 14
|
font.pointSize: 14
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
@@ -160,8 +197,8 @@ PanelWindow {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.notif.dismiss();
|
backgroundRect.modelData.close();
|
||||||
root.visible = false;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user