**animations** #5

Merged
Zacharias-Brohn merged 2 commits from ListView-NC-testing into main 2025-11-17 15:03:39 +01:00
8 changed files with 498 additions and 151 deletions
Showing only changes of commit e1469df7ff - Show all commits
+252
View File
@@ -0,0 +1,252 @@
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: notificationPopup
TrackedNotification {
centerX: NotificationCenter.posX
}
}
Component {
id: notifComp
Notif {}
}
}
+16
View File
@@ -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")
}
}
+20
View File
@@ -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
}
}
-45
View File
@@ -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
}
}
+52
View File
@@ -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
}
}
+156 -104
View File
@@ -9,6 +9,7 @@ import QtQuick
import Quickshell.Services.Notifications
import qs.Config
import qs.Helpers
import qs.Daemons
PanelWindow {
id: root
@@ -19,7 +20,6 @@ PanelWindow {
left: true
bottom: true
}
required property list<Notification> notifications
property bool centerShown: false
property alias posX: backgroundRect.x
property alias doNotDisturb: dndSwitch.checked
@@ -27,14 +27,6 @@ PanelWindow {
mask: Region { item: backgroundRect }
onNotificationsChanged: {
if ( root.notifications.length > 0 ) {
HasNotifications.hasNotifications = true;
} else {
HasNotifications.hasNotifications = false;
}
}
IpcHandler {
id: ipcHandler
target: "root"
@@ -88,38 +80,6 @@ PanelWindow {
easing.type: Easing.OutCubic
}
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 {
id: focusGrab
active: false
@@ -180,9 +140,8 @@ PanelWindow {
anchors.fill: parent
hoverEnabled: true
onClicked: {
for ( var app in groupedData.groups ) {
groupedData.groups[ app ].forEach( function( n ) { n.dismiss(); });
}
for ( const n of NotifServer.notClosed )
n.close();
}
}
}
@@ -198,6 +157,7 @@ PanelWindow {
Flickable {
Layout.fillWidth: true
Layout.fillHeight: true
pixelAligned: true
contentHeight: notificationColumn.implicitHeight
clip: true
@@ -223,52 +183,68 @@ PanelWindow {
}
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 {
id: groupColumn
required property string modelData
property var notifications: groupedData.groups[ modelData ]
property list<var> notifications: NotifServer.list.filter( n => n.appName === modelData )
width: parent.width
spacing: 10
property bool shouldShow: false
property bool isExpanded: false
function closeAll(): void {
for ( const n of NotifServer.notClosed.filter( n => n.appName === modelData ))
n.close();
}
Behavior on height {
Anim {}
}
add: Transition {
id: addTrans
SequentialAnimation {
PauseAnimation {
duration: ( addTrans.ViewTransition.index - addTrans.ViewTransition.targetIndexes[ 0 ]) * 50
}
ParallelAnimation {
NumberAnimation {
properties: "y";
from: addTrans.ViewTransition.destination.y - (height / 2);
to: addTrans.ViewTransition.destination.y;
duration: 100;
easing.type: Easing.OutCubic
}
NumberAnimation {
properties: "opacity";
from: 0;
to: 1;
duration: 100;
easing.type: Easing.OutCubic
}
}
}
}
// add: Transition {
// id: addTrans
// SequentialAnimation {
// PauseAnimation {
// duration: ( addTrans.ViewTransition.index - addTrans.ViewTransition.targetIndexes[ 0 ]) * 50
// }
// ParallelAnimation {
// NumberAnimation {
// properties: "y";
// from: addTrans.ViewTransition.destination.y - (height / 2);
// to: addTrans.ViewTransition.destination.y;
// duration: 100;
// easing.type: Easing.OutCubic
// }
// NumberAnimation {
// properties: "opacity";
// from: 0;
// to: 1;
// duration: 100;
// easing.type: Easing.OutCubic
// }
// }
// }
// }
//
move: Transition {
id: moveTrans
NumberAnimation {
properties: "opacity";
properties: "y";
duration: 100;
to: 0;
easing.type: Easing.OutCubic
}
}
@@ -311,40 +287,76 @@ PanelWindow {
}
}
Repeater {
model: groupColumn.notifications
Rectangle {
id: groupHeader
required property var modelData
required property var index
ListView {
id: groupListView
model: ScriptModel {
id: groupModel
values: groupColumn.isExpanded ? groupColumn.notifications : groupColumn.notifications.slice( 0, 1 )
}
width: parent.width
spacing: 10
height: contentHeight
contentHeight: childrenRect.height
clip: false
pixelAligned: true
boundsBehavior: Flickable.StopAtBounds
displayMarginBeginning: 0
displayMarginEnd: 5000
Behavior on height {
Anim {
duration: 20;
}
}
add: Transition {
id: add
NumberAnimation {
properties: "y,opacity";
duration: 100 * ( add.ViewTransition.targetIndexes.length / ( add.ViewTransition.targetIndexes.length < 3 ? 1 : 3 ));
easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial
}
}
remove: Transition {
NumberAnimation {
properties: "opacity";
from: 1;
to: 0;
duration: 300;
easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial
}
}
displaced: Transition {
NumberAnimation {
properties: "y";
duration: 200;
easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial
}
}
delegate: Rectangle {
id: groupHeader
required property int index
required property NotifServer.Notif modelData
property alias notifHeight: groupHeader.height
property bool previewHidden: !groupColumn.isExpanded && index > 0
width: groupListView.width
height: contentColumn.height + 20
color: Config.baseBgColor
border.color: "#555555"
border.width: 1
radius: 8
visible: groupColumn.notifications[0].id === modelData.id || groupColumn.isExpanded
opacity: groupColumn.notifications[0].id === modelData.id ? 1 : 0
opacity: 1
scale: 1.0
Connections {
target: groupColumn
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;
}
}
Component.onCompleted: modelData.lock(this);
Component.onDestruction: modelData.unlock(this);
MouseArea {
anchors.fill: parent
@@ -359,6 +371,44 @@ PanelWindow {
}
}
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 {
id: contentColumn
anchors.centerIn: parent
@@ -420,13 +470,15 @@ PanelWindow {
RowLayout {
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
width: parent.width
Repeater {
model: groupHeader.modelData.actions
Rectangle {
id: actionButton
Layout.fillWidth: true
Layout.preferredHeight: 30
required property var modelData
@@ -434,7 +486,7 @@ PanelWindow {
radius: 4
Text {
anchors.centerIn: parent
text: modelData.text
text: actionButton.modelData.text
color: "white"
font.pointSize: 12
}
@@ -443,7 +495,7 @@ PanelWindow {
anchors.fill: parent
hoverEnabled: true
onClicked: {
modelData.invoke();
actionButton.modelData.invoke();
}
}
}
@@ -472,7 +524,7 @@ PanelWindow {
anchors.fill: parent
hoverEnabled: true
onClicked: {
groupColumn.isExpanded ? groupColumn.notifications[0].dismiss() : groupColumn.notifications.forEach( function( n ) { n.dismiss(); });
groupColumn.isExpanded ? groupHeader.modelData.close() : groupColumn.closeAll();
}
}
}
+1 -1
View File
@@ -5,6 +5,7 @@ import QtQuick.Layouts
import QtQuick
import Quickshell.Services.Notifications
import qs.Config
import qs.Daemons
PanelWindow {
id: root
@@ -21,7 +22,6 @@ PanelWindow {
required property Notification notif
required property int centerX
required property list<int> notifIndex
required property list<TrackedNotification> notifList
property int index: notifIndex.indexOf(notif.id)
property alias y: backgroundRect.y
property alias notifHeight: backgroundRect.implicitHeight
+1 -1
View File
@@ -6,6 +6,6 @@ import qs.Modules
Scope {
Bar {}
Wallpaper {}
NotifServer {}
NotificationCenter {}
Launcher {}
}