test notif plugin #51
@@ -113,11 +113,12 @@ Singleton {
|
|||||||
id: storage
|
id: storage
|
||||||
|
|
||||||
path: `${Paths.state}/notifs.json`
|
path: `${Paths.state}/notifs.json`
|
||||||
|
printErrors: false
|
||||||
|
|
||||||
onLoadFailed: err => {
|
onLoadFailed: err => {
|
||||||
if (err === FileViewError.FileNotFound) {
|
if (err === FileViewError.FileNotFound) {
|
||||||
root.loaded = true;
|
root.loaded = true;
|
||||||
setText("[]");
|
Qt.callLater(() => setText("[]"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ CustomRect {
|
|||||||
required property var visibilities
|
required property var visibilities
|
||||||
|
|
||||||
color: {
|
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);
|
return expanded ? c : Qt.alpha(c, 0);
|
||||||
}
|
}
|
||||||
implicitHeight: nonAnimHeight
|
implicitHeight: nonAnimHeight
|
||||||
radius: 6
|
radius: 6
|
||||||
|
state: expanded ? "expanded" : ""
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
Behavior on implicitHeight {
|
||||||
Anim {
|
Anim {
|
||||||
@@ -33,7 +34,6 @@ CustomRect {
|
|||||||
}
|
}
|
||||||
states: State {
|
states: State {
|
||||||
name: "expanded"
|
name: "expanded"
|
||||||
when: root.expanded
|
|
||||||
|
|
||||||
PropertyChanges {
|
PropertyChanges {
|
||||||
compactBody.anchors.margins: 10
|
compactBody.anchors.margins: 10
|
||||||
@@ -63,10 +63,10 @@ CustomRect {
|
|||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.top: parent.top
|
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
|
elide: Text.ElideRight
|
||||||
maximumLineCount: 1
|
maximumLineCount: 1
|
||||||
text: root.modelData.summary
|
text: root.modelData?.summary ?? ""
|
||||||
width: parent.width
|
width: parent.width
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ CustomRect {
|
|||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
text: root.modelData.summary
|
text: root.modelData?.summary ?? ""
|
||||||
visible: false
|
visible: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +90,9 @@ CustomRect {
|
|||||||
shouldBeActive: !root.expanded
|
shouldBeActive: !root.expanded
|
||||||
|
|
||||||
sourceComponent: CustomText {
|
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
|
elide: Text.ElideRight
|
||||||
text: root.modelData.body.replace(/\n/g, " ")
|
text: String(root.modelData?.body ?? "").replace(/\n/g, " ")
|
||||||
textFormat: Text.StyledText
|
textFormat: Text.StyledText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ CustomRect {
|
|||||||
animate: true
|
animate: true
|
||||||
color: DynamicColors.palette.m3outline
|
color: DynamicColors.palette.m3outline
|
||||||
font.pointSize: 11
|
font.pointSize: 11
|
||||||
text: root.modelData.timeStr
|
text: root.modelData?.timeStr ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,8 +130,8 @@ CustomRect {
|
|||||||
id: body
|
id: body
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
color: root.modelData.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurface
|
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")
|
text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given")
|
||||||
textFormat: Text.MarkdownText
|
textFormat: Text.MarkdownText
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
@@ -148,14 +148,51 @@ CustomRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component WrappedLoader: Loader {
|
component WrappedLoader: Loader {
|
||||||
|
id: comp
|
||||||
|
|
||||||
required property bool shouldBeActive
|
required property bool shouldBeActive
|
||||||
|
|
||||||
active: opacity > 0
|
active: false
|
||||||
opacity: shouldBeActive ? 1 : 0
|
opacity: 0
|
||||||
|
|
||||||
Behavior on opacity {
|
states: State {
|
||||||
Anim {
|
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
|
isClose: true
|
||||||
},
|
},
|
||||||
...root.notif.actions,
|
...(root.notif?.actions ?? ""),
|
||||||
{
|
{
|
||||||
isCopy: true
|
isCopy: true
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ Item {
|
|||||||
id: actionInner
|
id: actionInner
|
||||||
|
|
||||||
anchors.centerIn: parent
|
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 {
|
Component {
|
||||||
|
|||||||
@@ -129,20 +129,27 @@ Item {
|
|||||||
Timer {
|
Timer {
|
||||||
id: clearTimer
|
id: clearTimer
|
||||||
|
|
||||||
interval: 50
|
interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(NotifServer.notClosed.length)))
|
||||||
repeat: true
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
let next = null;
|
const first = NotifServer.notClosed[0];
|
||||||
for (let i = 0; i < notifList.repeater.count; i++) {
|
if (!first) {
|
||||||
next = notifList.repeater.itemAt(i);
|
|
||||||
if (!next?.closed)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (next)
|
|
||||||
next.closeAll();
|
|
||||||
else
|
|
||||||
stop();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +1,47 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import ZShell.Components
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import Quickshell
|
|
||||||
import QtQuick
|
|
||||||
|
|
||||||
Item {
|
LazyListView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property Flickable container
|
required property Flickable container
|
||||||
property bool flag
|
|
||||||
required property Props props
|
required property Props props
|
||||||
readonly property alias repeater: repeater
|
required property DrawerVisibilities visibilities
|
||||||
readonly property int spacing: 8
|
|
||||||
required property var visibilities
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent?.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent?.right
|
||||||
implicitHeight: {
|
asynchronous: true
|
||||||
const item = repeater.itemAt(repeater.count - 1);
|
cacheBuffer: 400
|
||||||
return item ? item.y + item.implicitHeight : 0;
|
implicitHeight: contentHeight
|
||||||
}
|
readyDelay: 1
|
||||||
|
removeDuration: Appearance.anim.durations.normal
|
||||||
Repeater {
|
spacing: Appearance.spacing.small
|
||||||
id: repeater
|
useCustomViewport: true
|
||||||
|
viewport: Qt.rect(0, container.contentY, width, container.height)
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
delegate: Component {
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: notif
|
id: notif
|
||||||
|
|
||||||
readonly property bool closed: notifInner.notifCount === 0
|
readonly property bool closed: notifInner.notifCount === 0
|
||||||
required property int index
|
required property int index
|
||||||
required property string modelData
|
required property string modelData
|
||||||
readonly property alias nonAnimHeight: notifInner.nonAnimHeight
|
|
||||||
property int startY
|
property int startY
|
||||||
|
|
||||||
function closeAll(): void {
|
function closeAll(): void {
|
||||||
for (const n of NotifServer.notClosed.filter(n => n.appName === modelData)) {
|
clearTimer.start();
|
||||||
n.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
|
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
|
||||||
drag.axis: Drag.XAxis
|
drag.axis: Drag.XAxis
|
||||||
@@ -62,36 +49,32 @@ Item {
|
|||||||
enabled: !closed
|
enabled: !closed
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
implicitHeight: notifInner.implicitHeight
|
implicitHeight: notifInner.implicitHeight
|
||||||
implicitWidth: root.width
|
opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
y: {
|
scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
containmentMask: QtObject {
|
Behavior on opacity {
|
||||||
function contains(p: point): bool {
|
Anim {
|
||||||
if (!root.container.contains(notif.mapToItem(root.container, p)))
|
}
|
||||||
return false;
|
}
|
||||||
return notifInner.contains(p);
|
Behavior on scale {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
Anim {
|
Anim {
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
|
enabled: notif.LazyListView.ready
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,39 +99,22 @@ Item {
|
|||||||
closeAll();
|
closeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
ParallelAnimation {
|
Timer {
|
||||||
running: true
|
id: clearTimer
|
||||||
|
|
||||||
Anim {
|
interval: 15
|
||||||
from: 0
|
repeat: true
|
||||||
property: "opacity"
|
triggeredOnStart: true
|
||||||
target: notif
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
onTriggered: {
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
const notifs = Notifs.notClosed.filter(n => n.appName === notif.modelData);
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
if (notifs.length === 0) {
|
||||||
from: 0
|
stop();
|
||||||
property: "scale"
|
return;
|
||||||
target: notif
|
}
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ParallelAnimation {
|
for (const n of notifs.slice(0, 30))
|
||||||
running: notif.closed
|
n.close();
|
||||||
|
|
||||||
Anim {
|
|
||||||
property: "opacity"
|
|
||||||
target: notif
|
|
||||||
to: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
property: "scale"
|
|
||||||
target: notif
|
|
||||||
to: 0.6
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ CustomRect {
|
|||||||
required property string modelData
|
required property string modelData
|
||||||
readonly property int nonAnimHeight: {
|
readonly property int nonAnimHeight: {
|
||||||
const headerHeight = header.implicitHeight + (root.expanded ? Math.round(7 / 2) : 0);
|
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);
|
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)
|
readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
|
||||||
|
|||||||
@@ -8,113 +8,51 @@ import Quickshell
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
Item {
|
LazyListView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property Flickable container
|
required property Flickable container
|
||||||
required property bool expanded
|
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 list<var> notifs
|
||||||
required property Props props
|
required property Props props
|
||||||
property bool showAllNotifs
|
required property DrawerVisibilities visibilities
|
||||||
readonly property int spacing: Math.round(7 / 2)
|
|
||||||
required property var visibilities
|
|
||||||
|
|
||||||
signal requestToggleExpand(expand: bool)
|
signal requestToggleExpand(expand: bool)
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: nonAnimHeight
|
asynchronous: true
|
||||||
|
cacheBuffer: 400
|
||||||
Behavior on implicitHeight {
|
implicitHeight: contentHeight
|
||||||
Anim {
|
readyDelay: 1
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
removeDuration: Appearance.anim.durations.normal
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
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: {
|
delegate: Component {
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: notif
|
id: notif
|
||||||
|
|
||||||
required property int index
|
required property int index
|
||||||
required property NotifServer.Notif modelData
|
required property NotifData 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;
|
|
||||||
}
|
|
||||||
property int startY
|
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
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
||||||
drag.axis: Drag.XAxis
|
drag.axis: Drag.XAxis
|
||||||
drag.target: this
|
drag.target: this
|
||||||
enabled: !modelData.closed
|
enabled: !(modelData?.closed ?? true)
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
implicitHeight: notifInner.implicitHeight
|
implicitHeight: notifInner.implicitHeight
|
||||||
implicitWidth: root.width
|
opacity: LazyListView.removing || LazyListView.adding ? 0 : 1
|
||||||
opacity: previewHidden ? 0 : 1
|
|
||||||
preventStealing: !root.expanded
|
preventStealing: !root.expanded
|
||||||
scale: previewHidden ? 0.7 : 1
|
scale: LazyListView.removing || LazyListView.adding ? 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
}
|
}
|
||||||
@@ -125,19 +63,21 @@ Item {
|
|||||||
}
|
}
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
Anim {
|
Anim {
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
|
enabled: notif.LazyListView.ready
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: modelData.lock(this)
|
Component.onCompleted: modelData?.lock(this)
|
||||||
Component.onDestruction: modelData.unlock(this)
|
Component.onDestruction: modelData?.unlock(this)
|
||||||
onPositionChanged: event => {
|
onPositionChanged: event => {
|
||||||
if (pressed && !root.expanded) {
|
if (pressed && !root.expanded) {
|
||||||
const diffY = event.y - startY;
|
const diffY = event.y - startY;
|
||||||
@@ -150,37 +90,19 @@ Item {
|
|||||||
if (event.button === Qt.RightButton)
|
if (event.button === Qt.RightButton)
|
||||||
root.requestToggleExpand(!root.expanded);
|
root.requestToggleExpand(!root.expanded);
|
||||||
else if (event.button === Qt.MiddleButton)
|
else if (event.button === Qt.MiddleButton)
|
||||||
modelData.close();
|
modelData?.close();
|
||||||
}
|
}
|
||||||
onReleased: event => {
|
onReleased: event => {
|
||||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||||
x = 0;
|
x = 0;
|
||||||
else
|
else
|
||||||
modelData.close();
|
modelData?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
ParallelAnimation {
|
ParallelAnimation {
|
||||||
Component.onCompleted: running = !notif.previewHidden
|
running: notif.modelData?.closed ?? false
|
||||||
|
|
||||||
Anim {
|
onFinished: notif.modelData?.unlock(notif)
|
||||||
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)
|
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
property: "opacity"
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ CustomRect {
|
|||||||
|
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
|
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
|
||||||
implicitWidth: contentRow.implicitWidth + Appearance.spacing.smaller
|
implicitWidth: contentRow.implicitWidth + Appearance.spacing.small * 2
|
||||||
radius: height / 2
|
radius: height / 2
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
|||||||
@@ -56,3 +56,4 @@ qml_module(ZShell
|
|||||||
add_subdirectory(Models)
|
add_subdirectory(Models)
|
||||||
add_subdirectory(Internal)
|
add_subdirectory(Internal)
|
||||||
add_subdirectory(Services)
|
add_subdirectory(Services)
|
||||||
|
add_subdirectory(Components)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
qml_module(ZShell-components
|
||||||
|
URI ZShell.Components
|
||||||
|
SOURCES
|
||||||
|
lazylistview.hpp lazylistview.cpp
|
||||||
|
LIBRARIES
|
||||||
|
Qt::Quick
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,243 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qabstractitemmodel.h>
|
||||||
|
#include <qhash.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlcomponent.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qquickitem.h>
|
||||||
|
#include <qrect.h>
|
||||||
|
#include <qvector.h>
|
||||||
|
|
||||||
|
namespace ZShell::components {
|
||||||
|
|
||||||
|
class LazyListViewAttached : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged)
|
||||||
|
Q_PROPERTY(qreal visibleHeight READ visibleHeight WRITE setVisibleHeight NOTIFY visibleHeightChanged)
|
||||||
|
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
|
||||||
|
Q_PROPERTY(bool adding READ adding NOTIFY addingChanged)
|
||||||
|
Q_PROPERTY(bool removing READ removing NOTIFY removingChanged)
|
||||||
|
Q_PROPERTY(bool trackViewport READ trackViewport WRITE setTrackViewport NOTIFY trackViewportChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit LazyListViewAttached(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal preferredHeight() const;
|
||||||
|
void setPreferredHeight(qreal height);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal visibleHeight() const;
|
||||||
|
void setVisibleHeight(qreal height);
|
||||||
|
|
||||||
|
[[nodiscard]] bool ready() const;
|
||||||
|
void setReady(bool ready);
|
||||||
|
|
||||||
|
[[nodiscard]] bool adding() const;
|
||||||
|
void setAdding(bool adding);
|
||||||
|
|
||||||
|
[[nodiscard]] bool removing() const;
|
||||||
|
void setRemoving(bool removing);
|
||||||
|
|
||||||
|
[[nodiscard]] bool trackViewport() const;
|
||||||
|
void setTrackViewport(bool track);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void preferredHeightChanged();
|
||||||
|
void visibleHeightChanged();
|
||||||
|
void readyChanged();
|
||||||
|
void addingChanged();
|
||||||
|
void removingChanged();
|
||||||
|
void trackViewportChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
qreal m_preferredHeight = -1;
|
||||||
|
qreal m_visibleHeight = -1;
|
||||||
|
bool m_ready = false;
|
||||||
|
bool m_adding = false;
|
||||||
|
bool m_removing = false;
|
||||||
|
bool m_trackViewport = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LazyListView : public QQuickItem {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_ATTACHED(LazyListViewAttached)
|
||||||
|
|
||||||
|
// Model & Delegate
|
||||||
|
Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged)
|
||||||
|
Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged)
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
|
||||||
|
Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged)
|
||||||
|
Q_PROPERTY(qreal layoutHeight READ layoutHeight NOTIFY layoutHeightChanged)
|
||||||
|
Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged)
|
||||||
|
|
||||||
|
// Viewport & Lazy Loading
|
||||||
|
Q_PROPERTY(QRectF viewport READ viewport WRITE setViewport NOTIFY viewportChanged)
|
||||||
|
Q_PROPERTY(bool useCustomViewport READ useCustomViewport WRITE setUseCustomViewport NOTIFY useCustomViewportChanged)
|
||||||
|
Q_PROPERTY(qreal cacheBuffer READ cacheBuffer WRITE setCacheBuffer NOTIFY cacheBufferChanged)
|
||||||
|
|
||||||
|
// Sizing
|
||||||
|
Q_PROPERTY(qreal estimatedHeight READ estimatedHeight WRITE setEstimatedHeight NOTIFY estimatedHeightChanged)
|
||||||
|
|
||||||
|
// Async
|
||||||
|
Q_PROPERTY(bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged)
|
||||||
|
|
||||||
|
// Animation Durations
|
||||||
|
Q_PROPERTY(int removeDuration READ removeDuration WRITE setRemoveDuration NOTIFY removeDurationChanged)
|
||||||
|
Q_PROPERTY(int readyDelay READ readyDelay WRITE setReadyDelay NOTIFY readyDelayChanged)
|
||||||
|
|
||||||
|
// State
|
||||||
|
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit LazyListView(QQuickItem* parent = nullptr);
|
||||||
|
~LazyListView() override;
|
||||||
|
|
||||||
|
static LazyListViewAttached* qmlAttachedProperties(QObject* object);
|
||||||
|
|
||||||
|
// Model & Delegate
|
||||||
|
[[nodiscard]] QAbstractItemModel* model() const;
|
||||||
|
void setModel(QAbstractItemModel* model);
|
||||||
|
|
||||||
|
[[nodiscard]] QQmlComponent* delegate() const;
|
||||||
|
void setDelegate(QQmlComponent* delegate);
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
[[nodiscard]] qreal spacing() const;
|
||||||
|
void setSpacing(qreal spacing);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal contentHeight() const;
|
||||||
|
[[nodiscard]] qreal layoutHeight() const;
|
||||||
|
|
||||||
|
[[nodiscard]] qreal contentY() const;
|
||||||
|
void setContentY(qreal contentY);
|
||||||
|
|
||||||
|
// Viewport
|
||||||
|
[[nodiscard]] QRectF viewport() const;
|
||||||
|
void setViewport(const QRectF& viewport);
|
||||||
|
|
||||||
|
[[nodiscard]] bool useCustomViewport() const;
|
||||||
|
void setUseCustomViewport(bool use);
|
||||||
|
|
||||||
|
[[nodiscard]] qreal cacheBuffer() const;
|
||||||
|
void setCacheBuffer(qreal buffer);
|
||||||
|
|
||||||
|
// Sizing
|
||||||
|
[[nodiscard]] qreal estimatedHeight() const;
|
||||||
|
void setEstimatedHeight(qreal height);
|
||||||
|
|
||||||
|
// Async
|
||||||
|
[[nodiscard]] bool asynchronous() const;
|
||||||
|
void setAsynchronous(bool async);
|
||||||
|
|
||||||
|
// Animation Durations
|
||||||
|
[[nodiscard]] int removeDuration() const;
|
||||||
|
void setRemoveDuration(int duration);
|
||||||
|
|
||||||
|
[[nodiscard]] int readyDelay() const;
|
||||||
|
void setReadyDelay(int delay);
|
||||||
|
|
||||||
|
// State
|
||||||
|
[[nodiscard]] int count() const;
|
||||||
|
signals:
|
||||||
|
void modelChanged();
|
||||||
|
void delegateChanged();
|
||||||
|
void spacingChanged();
|
||||||
|
void contentHeightChanged();
|
||||||
|
void layoutHeightChanged();
|
||||||
|
void contentYChanged();
|
||||||
|
void viewportChanged();
|
||||||
|
void useCustomViewportChanged();
|
||||||
|
void cacheBufferChanged();
|
||||||
|
void estimatedHeightChanged();
|
||||||
|
void asynchronousChanged();
|
||||||
|
void removeDurationChanged();
|
||||||
|
void readyDelayChanged();
|
||||||
|
void countChanged();
|
||||||
|
void viewportAdjustNeeded(qreal delta);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void componentComplete() override;
|
||||||
|
void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override;
|
||||||
|
void updatePolish() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct ItemRecord {
|
||||||
|
qreal targetY = 0;
|
||||||
|
qreal height = 0;
|
||||||
|
bool heightKnown = false;
|
||||||
|
bool isNew = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DelegateEntry {
|
||||||
|
int modelIndex = -1;
|
||||||
|
QQuickItem* item = nullptr;
|
||||||
|
bool pendingRemoval = false;
|
||||||
|
bool pendingInsert = false;
|
||||||
|
bool readyDelayStarted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
void relayout();
|
||||||
|
[[nodiscard]] std::pair<int, int> computeVisibleRange() const;
|
||||||
|
[[nodiscard]] QRectF effectiveViewport() const;
|
||||||
|
[[nodiscard]] qreal effectiveEstimatedHeight() const;
|
||||||
|
[[nodiscard]] static qreal delegateHeight(QQuickItem* item);
|
||||||
|
[[nodiscard]] static qreal delegateVisibleHeight(QQuickItem* item);
|
||||||
|
[[nodiscard]] static bool isDelegateReady(QQuickItem* item);
|
||||||
|
void trackHeight(qreal height);
|
||||||
|
void untrackHeight(qreal height);
|
||||||
|
|
||||||
|
// Delegate lifecycle
|
||||||
|
void syncDelegates();
|
||||||
|
DelegateEntry createDelegate(int modelIndex);
|
||||||
|
void destroyDelegate(DelegateEntry& entry);
|
||||||
|
void updateDelegateData(DelegateEntry& entry);
|
||||||
|
|
||||||
|
// Model connection
|
||||||
|
void connectModel();
|
||||||
|
void disconnectModel();
|
||||||
|
void resetContent();
|
||||||
|
void onRowsInserted(const QModelIndex& parent, int first, int last);
|
||||||
|
void onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last);
|
||||||
|
void onRowsRemoved(const QModelIndex& parent, int first, int last);
|
||||||
|
void onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row);
|
||||||
|
void onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList<int>& roles);
|
||||||
|
void onModelReset();
|
||||||
|
|
||||||
|
// Members
|
||||||
|
QAbstractItemModel* m_model = nullptr;
|
||||||
|
QQmlComponent* m_delegate = nullptr;
|
||||||
|
|
||||||
|
qreal m_spacing = 0;
|
||||||
|
qreal m_contentHeight = 0;
|
||||||
|
qreal m_layoutHeight = 0;
|
||||||
|
qreal m_contentY = 0;
|
||||||
|
|
||||||
|
QRectF m_viewport;
|
||||||
|
bool m_useCustomViewport = false;
|
||||||
|
qreal m_cacheBuffer = 0;
|
||||||
|
|
||||||
|
qreal m_estimatedHeight = -1;
|
||||||
|
qreal m_knownHeightSum = 0;
|
||||||
|
int m_knownHeightCount = 0;
|
||||||
|
bool m_asynchronous = false;
|
||||||
|
|
||||||
|
int m_removeDuration = 300;
|
||||||
|
int m_readyDelay = 0;
|
||||||
|
|
||||||
|
QVector<ItemRecord> m_layout;
|
||||||
|
QHash<int, DelegateEntry> m_delegates;
|
||||||
|
QHash<QQuickItem*, int> m_itemToIndex;
|
||||||
|
QVector<DelegateEntry> m_dyingDelegates;
|
||||||
|
|
||||||
|
bool m_componentComplete = false;
|
||||||
|
bool m_relayoutPending = false;
|
||||||
|
|
||||||
|
QList<QMetaObject::Connection> m_modelConnections;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::components
|
||||||
Reference in New Issue
Block a user