test notif plugin

This commit is contained in:
2026-04-12 19:28:20 +02:00
parent 8bd8a7dad7
commit 487c56bc47
12 changed files with 1564 additions and 221 deletions
+51 -14
View File
@@ -19,11 +19,12 @@ CustomRect {
required property var visibilities
color: {
const c = root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2);
const c = root.modelData?.urgency === "critical" ? DynamicColors.palette.m3secondaryContainer : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2);
return expanded ? c : Qt.alpha(c, 0);
}
implicitHeight: nonAnimHeight
radius: 6
state: expanded ? "expanded" : ""
Behavior on implicitHeight {
Anim {
@@ -33,7 +34,6 @@ CustomRect {
}
states: State {
name: "expanded"
when: root.expanded
PropertyChanges {
compactBody.anchors.margins: 10
@@ -63,10 +63,10 @@ CustomRect {
anchors.left: parent.left
anchors.top: parent.top
color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
color: root.modelData?.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
elide: Text.ElideRight
maximumLineCount: 1
text: root.modelData.summary
text: root.modelData?.summary ?? ""
width: parent.width
wrapMode: Text.WordWrap
}
@@ -76,7 +76,7 @@ CustomRect {
anchors.left: parent.left
anchors.top: parent.top
text: root.modelData.summary
text: root.modelData?.summary ?? ""
visible: false
}
@@ -90,9 +90,9 @@ CustomRect {
shouldBeActive: !root.expanded
sourceComponent: CustomText {
color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline
color: root.modelData?.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline
elide: Text.ElideRight
text: root.modelData.body.replace(/\n/g, " ")
text: String(root.modelData?.body ?? "").replace(/\n/g, " ")
textFormat: Text.StyledText
}
}
@@ -108,7 +108,7 @@ CustomRect {
animate: true
color: DynamicColors.palette.m3outline
font.pointSize: 11
text: root.modelData.timeStr
text: root.modelData?.timeStr ?? ""
}
}
@@ -130,8 +130,8 @@ CustomRect {
id: body
Layout.fillWidth: true
color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurface
text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given")
color: root.modelData?.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurface
text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given")
textFormat: Text.MarkdownText
wrapMode: Text.WordWrap
@@ -148,14 +148,51 @@ CustomRect {
}
component WrappedLoader: Loader {
id: comp
required property bool shouldBeActive
active: opacity > 0
opacity: shouldBeActive ? 1 : 0
active: false
opacity: 0
Behavior on opacity {
Anim {
states: State {
name: "active"
when: comp.shouldBeActive
PropertyChanges {
comp.active: true
comp.opacity: 1
}
}
transitions: [
Transition {
from: ""
to: "active"
SequentialAnimation {
PropertyAction {
property: "active"
}
Anim {
property: "opacity"
}
}
},
Transition {
from: "active"
to: ""
SequentialAnimation {
Anim {
property: "opacity"
}
PropertyAction {
property: "active"
}
}
}
]
}
}
@@ -35,7 +35,7 @@ Item {
{
isClose: true
},
...root.notif.actions,
...(root.notif?.actions ?? ""),
{
isCopy: true
}
@@ -97,7 +97,7 @@ Item {
id: actionInner
anchors.centerIn: parent
sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp
sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif?.hasActionIcons ? iconComp : textComp
}
Component {
+17 -10
View File
@@ -129,20 +129,27 @@ Item {
Timer {
id: clearTimer
interval: 50
interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(NotifServer.notClosed.length)))
repeat: true
triggeredOnStart: true
onTriggered: {
let next = null;
for (let i = 0; i < notifList.repeater.count; i++) {
next = notifList.repeater.itemAt(i);
if (!next?.closed)
break;
}
if (next)
next.closeAll();
else
const first = NotifServer.notClosed[0];
if (!first) {
stop();
return;
}
const appName = first.appName;
let cleared = 0;
for (const n of NotifServer.notClosed.filter(n => n.appName === appName)) {
n.close();
cleared++;
if (cleared > 30) {
interval = 5;
return;
}
}
}
}
+75 -83
View File
@@ -1,60 +1,47 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import ZShell.Components
import qs.Components
import qs.Config
import qs.Modules
import qs.Daemons
import Quickshell
import QtQuick
Item {
LazyListView {
id: root
required property Flickable container
property bool flag
required property Props props
readonly property alias repeater: repeater
readonly property int spacing: 8
required property var visibilities
required property DrawerVisibilities visibilities
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: {
const item = repeater.itemAt(repeater.count - 1);
return item ? item.y + item.implicitHeight : 0;
}
Repeater {
id: repeater
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()
}
anchors.left: parent?.left
anchors.right: parent?.right
asynchronous: true
cacheBuffer: 400
implicitHeight: contentHeight
readyDelay: 1
removeDuration: Appearance.anim.durations.normal
spacing: Appearance.spacing.small
useCustomViewport: true
viewport: Qt.rect(0, container.contentY, width, container.height)
delegate: Component {
MouseArea {
id: notif
readonly property bool closed: notifInner.notifCount === 0
required property int index
required property string modelData
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
property int startY
function closeAll(): void {
for (const n of NotifServer.notClosed.filter(n => n.appName === modelData)) {
n.close();
}
clearTimer.start();
}
LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight
LazyListView.trackViewport: !notifInner.expanded && notifInner.nonAnimHeight < notifInner.implicitHeight
LazyListView.visibleHeight: notifInner.implicitHeight
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
drag.axis: Drag.XAxis
@@ -62,36 +49,32 @@ Item {
enabled: !closed
hoverEnabled: true
implicitHeight: notifInner.implicitHeight
implicitWidth: root.width
opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1
preventStealing: true
y: {
root.flag; // Force update
let y = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i);
if (!item.closed)
y += item.nonAnimHeight + root.spacing;
}
return y;
}
scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1
containmentMask: QtObject {
function contains(p: point): bool {
if (!root.container.contains(notif.mapToItem(root.container, p)))
return false;
return notifInner.contains(p);
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on x {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on y {
enabled: notif.LazyListView.ready
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
@@ -116,39 +99,22 @@ Item {
closeAll();
}
ParallelAnimation {
running: true
Timer {
id: clearTimer
Anim {
from: 0
property: "opacity"
target: notif
to: 1
}
interval: 15
repeat: true
triggeredOnStart: true
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
from: 0
property: "scale"
target: notif
to: 1
}
}
onTriggered: {
const notifs = Notifs.notClosed.filter(n => n.appName === notif.modelData);
if (notifs.length === 0) {
stop();
return;
}
ParallelAnimation {
running: notif.closed
Anim {
property: "opacity"
target: notif
to: 0
}
Anim {
property: "scale"
target: notif
to: 0.6
for (const n of notifs.slice(0, 30))
n.close();
}
}
@@ -162,4 +128,30 @@ Item {
}
}
}
model: ScriptModel {
values: {
const map = new Map();
for (const n of Notifs.notClosed)
map.set(n.appName, null);
for (const n of Notifs.list)
map.set(n.appName, null);
return [...map.keys()];
}
}
onViewportAdjustNeeded: d => {
if (contentYAnim.running)
contentYAnim.complete();
contentYAnim.to = Math.max(0, container.contentY + d);
contentYAnim.start();
}
Anim {
id: contentYAnim
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
property: "contentY"
target: root.container
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ CustomRect {
required property string modelData
readonly property int nonAnimHeight: {
const headerHeight = header.implicitHeight + (root.expanded ? Math.round(7 / 2) : 0);
const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin;
const columnHeight = headerHeight + notifList.layoutHeight + column.Layout.topMargin + column.Layout.bottomMargin;
return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + 10 * 2);
}
readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
+55 -109
View File
@@ -8,113 +8,51 @@ import Quickshell
import QtQuick
import QtQuick.Layouts
Item {
LazyListView {
id: root
required property Flickable container
required property bool expanded
property bool flag
readonly property real nonAnimHeight: {
let h = -root.spacing;
for (let i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i);
if (!item.modelData.closed && !item.previewHidden)
h += item.nonAnimHeight + root.spacing;
}
return h;
}
required property list<var> notifs
required property Props props
property bool showAllNotifs
readonly property int spacing: Math.round(7 / 2)
required property var visibilities
required property DrawerVisibilities visibilities
signal requestToggleExpand(expand: bool)
Layout.fillWidth: true
implicitHeight: nonAnimHeight
Behavior on implicitHeight {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
}
asynchronous: true
cacheBuffer: 400
implicitHeight: contentHeight
readyDelay: 1
removeDuration: Appearance.anim.durations.normal
spacing: Math.round(Appearance.spacing.small / 2)
useCustomViewport: true
viewport: {
tWatcher.transform; // mapToItem is not reactive so use this to trigger updates
return Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height);
}
onExpandedChanged: {
if (expanded) {
clearTimer.stop();
showAllNotifs = true;
} else {
clearTimer.start();
}
}
Timer {
id: clearTimer
interval: MaterialEasing.standardTime
onTriggered: root.showAllNotifs = false
}
Repeater {
id: repeater
model: ScriptModel {
values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1)
onValuesChanged: root.flagChanged()
}
delegate: Component {
MouseArea {
id: notif
required property int index
required property NotifServer.Notif modelData
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
readonly property bool previewHidden: {
if (root.expanded)
return false;
let extraHidden = 0;
for (let i = 0; i < index; i++)
if (root.notifs[i].closed)
extraHidden++;
return index >= Config.notifs.groupPreviewNum + extraHidden;
}
required property NotifData modelData
property int startY
LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight
LazyListView.visibleHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.implicitHeight
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
drag.axis: Drag.XAxis
drag.target: this
enabled: !modelData.closed
enabled: !(modelData?.closed ?? true)
hoverEnabled: true
implicitHeight: notifInner.implicitHeight
implicitWidth: root.width
opacity: previewHidden ? 0 : 1
opacity: LazyListView.removing || LazyListView.adding ? 0 : 1
preventStealing: !root.expanded
scale: previewHidden ? 0.7 : 1
y: {
root.flag; // Force update
let y = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i);
if (!item.modelData.closed && !item.previewHidden)
y += item.nonAnimHeight + root.spacing;
}
return y;
}
scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1
containmentMask: QtObject {
function contains(p: point): bool {
if (!root.container.contains(notif.mapToItem(root.container, p)))
return false;
return notifInner.contains(p);
}
}
Behavior on opacity {
Anim {
}
@@ -125,19 +63,21 @@ Item {
}
Behavior on x {
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on y {
enabled: notif.LazyListView.ready
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Component.onCompleted: modelData.lock(this)
Component.onDestruction: modelData.unlock(this)
Component.onCompleted: modelData?.lock(this)
Component.onDestruction: modelData?.unlock(this)
onPositionChanged: event => {
if (pressed && !root.expanded) {
const diffY = event.y - startY;
@@ -150,37 +90,19 @@ Item {
if (event.button === Qt.RightButton)
root.requestToggleExpand(!root.expanded);
else if (event.button === Qt.MiddleButton)
modelData.close();
modelData?.close();
}
onReleased: event => {
if (Math.abs(x) < width * Config.notifs.clearThreshold)
x = 0;
else
modelData.close();
modelData?.close();
}
ParallelAnimation {
Component.onCompleted: running = !notif.previewHidden
running: notif.modelData?.closed ?? false
Anim {
from: 0
property: "opacity"
target: notif
to: 1
}
Anim {
from: 0.7
property: "scale"
target: notif
to: 1
}
}
ParallelAnimation {
running: notif.modelData.closed
onFinished: notif.modelData.unlock(notif)
onFinished: notif.modelData?.unlock(notif)
Anim {
property: "opacity"
@@ -206,4 +128,28 @@ Item {
}
}
}
model: ScriptModel {
values: {
if (root.expanded)
return root.notifs;
let count = 0;
let i = 0;
const previewNum = Config.notifs.groupPreviewNum;
while (i < root.notifs.length && count < previewNum) {
if (!(root.notifs[i]?.closed ?? true))
count++;
i++;
}
return root.notifs.slice(0, i);
}
}
TransformWatcher {
id: tWatcher
a: root.container.contentItem
b: root
}
}