feat: better notification storage and image caching + blobs
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 11s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m26s
Rust / fmt (pull_request) Successful in 46s
Rust / clippy (pull_request) Successful in 1m6s
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 11s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m26s
Rust / fmt (pull_request) Successful in 46s
Rust / clippy (pull_request) Successful in 1m6s
This commit is contained in:
@@ -13,6 +13,7 @@ Item {
|
||||
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
|
||||
required property bool horizontal
|
||||
required property PopoutState popouts
|
||||
required property Wrapper wrapper
|
||||
|
||||
implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.smaller * 2
|
||||
implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.smaller * 2
|
||||
@@ -98,10 +99,10 @@ Item {
|
||||
readonly property bool shouldBeActive: root.popouts.currentName === name
|
||||
|
||||
active: false
|
||||
anchors.centerIn: opacity === 1 ? undefined : parent
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: opacity === 1 ? parent.top : undefined
|
||||
anchors.topMargin: Tokens.padding.smaller
|
||||
// anchors.top: opacity === 1 ? parent.top : undefined
|
||||
// anchors.topMargin: Tokens.padding.smaller
|
||||
opacity: 0
|
||||
scale: 0.8
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ StackView {
|
||||
id: root
|
||||
|
||||
readonly property int itemHeight: 30
|
||||
readonly property int panelRadius: ((itemHeight / 2) + Tokens.padding.extraSmall) * Tokens.rounding.scale
|
||||
readonly property int panelRadius: ((itemHeight / 2) + Tokens.padding.smaller) * Tokens.rounding.scale
|
||||
required property PopoutState popouts
|
||||
property int rootWidth: 0
|
||||
required property QsMenuHandle trayItem
|
||||
@@ -27,20 +27,15 @@ StackView {
|
||||
initialItem: SubMenu {
|
||||
handle: root.trayItem
|
||||
}
|
||||
popEnter: NoAnim {
|
||||
}
|
||||
popExit: NoAnim {
|
||||
}
|
||||
pushEnter: NoAnim {
|
||||
}
|
||||
pushExit: NoAnim {
|
||||
}
|
||||
popEnter: NoAnim {}
|
||||
popExit: NoAnim {}
|
||||
pushEnter: NoAnim {}
|
||||
pushExit: NoAnim {}
|
||||
|
||||
Component {
|
||||
id: subMenuComp
|
||||
|
||||
SubMenu {
|
||||
}
|
||||
SubMenu {}
|
||||
}
|
||||
|
||||
component NoAnim: Transition {
|
||||
@@ -49,8 +44,8 @@ StackView {
|
||||
}
|
||||
}
|
||||
component SubMenu: SnapLayout {
|
||||
orientation: Qt.Vertical
|
||||
id: menu
|
||||
orientation: Qt.Vertical
|
||||
|
||||
required property QsMenuHandle handle
|
||||
property bool isSubMenu
|
||||
@@ -61,12 +56,10 @@ orientation: Qt.Vertical
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Component.onCompleted: shown = true
|
||||
|
||||
@@ -58,6 +58,7 @@ Item {
|
||||
shouldBeActive: root.hasCurrent
|
||||
|
||||
sourceComponent: Content {
|
||||
wrapper: root
|
||||
horizontal: root.horizontal
|
||||
popouts: popoutState
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ Item {
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
orientation: Qt.Vertical
|
||||
id: layout
|
||||
orientation: Qt.Vertical
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Tokens.padding.large
|
||||
@@ -77,7 +77,7 @@ orientation: Qt.Vertical
|
||||
|
||||
required property string icon
|
||||
|
||||
implicitSize: ZUtils.snap((root.height - layout.anchors.margins * 2 - layout.spacing * 4) / 5, root)
|
||||
implicitSize: (root.height - layout.anchors.margins * 2 - layout.spacing * 4) / 5
|
||||
|
||||
Behavior on clampedVal {
|
||||
Anim {}
|
||||
|
||||
@@ -42,7 +42,7 @@ SnapLayout {
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
orientation: Qt.Vertical
|
||||
orientation: Qt.Vertical
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: root.width / 2
|
||||
@@ -71,11 +71,11 @@ orientation: Qt.Vertical
|
||||
id: tempGraph
|
||||
|
||||
anchors.fill: parent
|
||||
values: root.forecastToday.map(e => e.tempC)
|
||||
values: root.forecastToday.map(e => e?.tempC ?? null)
|
||||
verticalLineStep: 3
|
||||
xSteps: 24
|
||||
horizontalLineStep: 10
|
||||
minHorizontalLines: 4
|
||||
minHorizontalLines: 3
|
||||
yPaddingTop: horizontalLineStep
|
||||
yPaddingBottom: horizontalLineStep
|
||||
xLabel: i => root.formatHour(i)
|
||||
@@ -95,7 +95,7 @@ orientation: Qt.Vertical
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
orientation: Qt.Vertical
|
||||
orientation: Qt.Vertical
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: root.width / 2
|
||||
@@ -132,7 +132,7 @@ orientation: Qt.Vertical
|
||||
yPaddingTop: 20
|
||||
verticalLineStep: 3
|
||||
lastXLine: false
|
||||
horizontalLineStep: 20
|
||||
horizontalLineStep: 30
|
||||
xLabel: i => root.formatHour(i)
|
||||
yLabel: v => v + "%"
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Services
|
||||
import qs.Modules
|
||||
import qs.Components
|
||||
import qs.Components.Notifs
|
||||
import qs.Helpers
|
||||
import qs.Daemons
|
||||
import qs.Services
|
||||
@@ -34,7 +36,7 @@ CustomRect {
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
|
||||
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
|
||||
text: NotifServer.notClosed.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.notClosed.length).arg(NotifServer.notClosed.length === 1 ? "" : "s") : qsTr("Notifications")
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -44,7 +46,7 @@ CustomRect {
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
opacity: NotifServer.list.length > 0 ? 0 : 1
|
||||
opacity: NotifServer.notClosed.length > 0 ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
@@ -59,8 +61,11 @@ CustomRect {
|
||||
}
|
||||
|
||||
CustomListView {
|
||||
id: list
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
orientation: Qt.Vertical
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
add: Transition {
|
||||
@@ -78,7 +83,7 @@ CustomRect {
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
delegate: NotifGroup {}
|
||||
delegate: NotifRow {}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
@@ -92,10 +97,7 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const list = NotifServer.notClosed.map(n => [n.appName, null]);
|
||||
return [...new Map(list).keys()];
|
||||
}
|
||||
values: [...NotifServer.groups].filter(g => g.count > 0)
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
@@ -123,4 +125,51 @@ CustomRect {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component NotifRow: Item {
|
||||
id: row
|
||||
|
||||
required property NotificationGroup modelData
|
||||
property bool expanded: false
|
||||
|
||||
function dismissAll(): void {
|
||||
for (const n of [...modelData.activeNotifs])
|
||||
n.dismiss();
|
||||
}
|
||||
|
||||
implicitHeight: card.implicitHeight
|
||||
implicitWidth: list.width
|
||||
|
||||
NotifSwipeArea {
|
||||
id: area
|
||||
|
||||
implicitHeight: card.implicitHeight
|
||||
implicitWidth: row.implicitWidth
|
||||
|
||||
expanded: row.expanded
|
||||
onDismissed: row.dismissAll()
|
||||
onRequestExpand: expand => row.expanded = expand
|
||||
onSwiped: row.dismissAll()
|
||||
|
||||
Item {
|
||||
anchors.top: parent.top
|
||||
|
||||
implicitHeight: card.implicitHeight
|
||||
implicitWidth: row.implicitWidth
|
||||
|
||||
clip: true
|
||||
|
||||
NotifGroupCard {
|
||||
id: card
|
||||
|
||||
baseLayer: 1
|
||||
container: list
|
||||
expanded: row.expanded
|
||||
group: row.modelData
|
||||
onRequestExpand: expand => row.expanded = expand
|
||||
onSwiped: notif => notif.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Modules
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Daemons
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
|
||||
property bool expanded
|
||||
readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? ""
|
||||
required property string modelData
|
||||
readonly property list<var> notifs: NotifServer.list.filter(notif => notif.appName === modelData)
|
||||
readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low"
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
clip: true
|
||||
color: root.urgency === "critical" ? Colors.palette.m3secondaryContainer : Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
|
||||
radius: Tokens.rounding.medium
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
id: content
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
implicitHeight: Config.notifs.sizes.image
|
||||
implicitWidth: Config.notifs.sizes.image
|
||||
|
||||
Component {
|
||||
id: imageComp
|
||||
|
||||
Image {
|
||||
asynchronous: true
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
height: Config.notifs.sizes.image
|
||||
source: Qt.resolvedUrl(root.image)
|
||||
visible: Config.lock.showNotifIcon
|
||||
width: Config.notifs.sizes.image
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appIconComp
|
||||
|
||||
ColoredIcon {
|
||||
color: root.urgency === "critical" ? Colors.palette.m3onError : root.urgency === "low" ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
implicitSize: Math.round(Config.notifs.sizes.image * 0.6)
|
||||
layer.enabled: root.appIcon.endsWith("symbolic")
|
||||
source: Quickshell.iconPath(root.appIcon)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: materialIconComp
|
||||
|
||||
MaterialIcon {
|
||||
color: root.urgency === "critical" ? Colors.palette.m3onError : root.urgency === "low" ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
fontStyle: Tokens.font.icon.large
|
||||
text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency)
|
||||
}
|
||||
}
|
||||
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
color: root.urgency === "critical" ? Colors.palette.m3error : root.urgency === "low" ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 3) : Colors.palette.m3secondaryContainer
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Loader {
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.appIcon && root.image
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
color: root.urgency === "critical" ? Colors.palette.m3error : root.urgency === "low" ? Colors.palette.m3surfaceContainerHighest : Colors.palette.m3secondaryContainer
|
||||
implicitHeight: Config.notifs.sizes.badge
|
||||
implicitWidth: Config.notifs.sizes.badge
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
ColoredIcon {
|
||||
anchors.centerIn: parent
|
||||
color: root.urgency === "critical" ? Colors.palette.m3onError : root.urgency === "low" ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
implicitSize: Math.round(Config.notifs.sizes.badge * 0.6)
|
||||
layer.enabled: root.appIcon.endsWith("symbolic")
|
||||
source: Quickshell.iconPath(root.appIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
Layout.bottomMargin: -Tokens.padding.extraSmall / 2 - (root.expanded ? 0 : spacing)
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -Tokens.padding.extraSmall
|
||||
|
||||
spacing: Math.round(Tokens.spacing.small / 2)
|
||||
orientation: Qt.Vertical
|
||||
|
||||
SnapLayout {
|
||||
Layout.bottomMargin: -parent.spacing
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.body.small
|
||||
text: root.modelData
|
||||
}
|
||||
|
||||
CustomText {
|
||||
animate: true
|
||||
color: Colors.palette.m3outline
|
||||
font: Tokens.font.body.small
|
||||
text: root.notifs[0]?.timeStr ?? ""
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0
|
||||
color: root.urgency === "critical" ? Colors.palette.m3error : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
||||
implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall
|
||||
implicitWidth: expandBtn.implicitWidth + Tokens.padding.small * 2
|
||||
opacity: root.notifs.length > Config.notifs.groupPreviewNum ? 1 : 0
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: root.urgency === "critical" ? Colors.palette.m3onError : Colors.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
root.expanded = !root.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
id: expandBtn
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
spacing: Tokens.spacing.small / 2
|
||||
|
||||
CustomText {
|
||||
id: groupCount
|
||||
|
||||
Layout.leftMargin: Tokens.padding.extraSmall / 2
|
||||
animate: true
|
||||
color: root.urgency === "critical" ? Colors.palette.m3onError : Colors.palette.m3onSurface
|
||||
font: Tokens.font.body.small
|
||||
text: root.notifs.length
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.rightMargin: -Tokens.padding.extraSmall / 2
|
||||
animate: true
|
||||
color: root.urgency === "critical" ? Colors.palette.m3onError : Colors.palette.m3onSurface
|
||||
text: root.expanded ? "expand_less" : "expand_more"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.notifs.slice(0, Config.notifs.groupPreviewNum)
|
||||
}
|
||||
|
||||
NotifLine {
|
||||
id: notif
|
||||
|
||||
ParallelAnimation {
|
||||
running: true
|
||||
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
target: notif
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
from: 0.7
|
||||
property: "scale"
|
||||
target: notif
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
from: 0
|
||||
property: "preferredHeight"
|
||||
target: notif.Layout
|
||||
to: notif.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: notif.modelData.closed
|
||||
|
||||
onFinished: notif.modelData.unlock(notif)
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
target: notif
|
||||
to: 0
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "scale"
|
||||
target: notif
|
||||
to: 0.7
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "preferredHeight"
|
||||
target: notif.Layout
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.expanded ? implicitHeight : 0
|
||||
active: opacity > 0
|
||||
opacity: root.expanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
sourceComponent: SnapLayout {
|
||||
orientation: Qt.Vertical
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.notifs.slice(Config.notifs.groupPreviewNum)
|
||||
}
|
||||
|
||||
NotifLine {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component NotifLine: CustomText {
|
||||
id: notifLine
|
||||
|
||||
required property NotificationRow modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: root.urgency === "critical" ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface
|
||||
text: {
|
||||
if (!Config.lock.showNotifContent)
|
||||
return "Unlock to view";
|
||||
const summary = modelData.summary.replace(/\n/g, " ");
|
||||
const body = modelData.body.replace(/\n/g, " ");
|
||||
const color = root.urgency === "critical" ? Colors.palette.m3secondary : Colors.palette.m3outline;
|
||||
|
||||
if (metrics.text === metrics.elidedText)
|
||||
return `${summary} <span style='color:${color}'>${body}</span>`;
|
||||
|
||||
const t = metrics.elidedText.length - 3;
|
||||
if (t < summary.length)
|
||||
return `${summary.slice(0, t)}...`;
|
||||
|
||||
return `${summary} <span style='color:${color}'>${body.slice(0, t - summary.length)}...</span>`;
|
||||
}
|
||||
textFormat: Text.MarkdownText
|
||||
|
||||
Component.onCompleted: modelData.lock(this)
|
||||
Component.onDestruction: modelData.unlock(this)
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: notifLine.width
|
||||
font.family: notifLine.font.family
|
||||
font.pointSize: notifLine.font.pointSize
|
||||
text: `${notifLine.modelData.summary} ${notifLine.modelData.body}`.replace(/\n/g, " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,14 @@ pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Components.Notifs
|
||||
import qs.Daemons
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -21,13 +23,10 @@ Item {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
implicitHeight: {
|
||||
const count = list.count;
|
||||
if (count === 0)
|
||||
if (list.count === 0)
|
||||
return 0;
|
||||
|
||||
let height = (count - 1) * Tokens.spacing.small;
|
||||
for (let i = 0; i < count; i++)
|
||||
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
|
||||
let height = list.layoutHeight;
|
||||
|
||||
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x)
|
||||
return 0;
|
||||
@@ -54,36 +53,55 @@ Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.padding
|
||||
clip: true
|
||||
// radius: Tokens.rounding.large - root.padding
|
||||
|
||||
CustomListView {
|
||||
id: list
|
||||
KineticFlickable {
|
||||
id: view
|
||||
|
||||
anchors.fill: parent
|
||||
cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0
|
||||
orientation: Qt.Vertical
|
||||
spacing: 0
|
||||
|
||||
delegate: NotifWrapper {}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
const popups = NotifServer.popups.filter(n => !n.closed);
|
||||
contentHeight: list.implicitHeight
|
||||
contentWidth: width
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
if (!Config.notifs.showInFullscreen) {
|
||||
if (Hypr.monitorFor(root.panels.screen).activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false)
|
||||
return [];
|
||||
LazyListView {
|
||||
id: list
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: contentHeight
|
||||
|
||||
spacing: Tokens.spacing.medium
|
||||
readyDelay: 1
|
||||
cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0
|
||||
removeDuration: Tokens.anim.durations.normal
|
||||
|
||||
useCustomViewport: true
|
||||
viewport: Qt.rect(0, view.contentY, width, view.height)
|
||||
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
if (!Config.notifs.showInFullscreen) {
|
||||
if (Hypr.monitorFor(root.panels.screen).activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false)
|
||||
return [];
|
||||
}
|
||||
return [...NotifServer.popupGroups];
|
||||
}
|
||||
return popups;
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
|
||||
delegate: NotifWrapper {}
|
||||
|
||||
onViewportAdjustNeeded: d => {
|
||||
if (contentYAnim.running)
|
||||
contentYAnim.complete();
|
||||
contentYAnim.to = Math.max(0, view.contentY + d);
|
||||
contentYAnim.start();
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "y"
|
||||
id: contentYAnim
|
||||
|
||||
target: view
|
||||
property: "contentY"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,74 +114,106 @@ Item {
|
||||
component NotifWrapper: Item {
|
||||
id: wrapper
|
||||
|
||||
property int idx
|
||||
required property int index
|
||||
required property NotificationRow modelData
|
||||
readonly property alias nonAnimHeight: notif.nonAnimHeight
|
||||
required property NotificationGroup modelData
|
||||
property bool expanded: Config.notifs.openExpanded
|
||||
readonly property bool holding: area.containsMouse || area.pressed || card.rowPressed
|
||||
|
||||
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.small)
|
||||
implicitWidth: notif.implicitWidth
|
||||
|
||||
ListView.onRemove: removeAnim.start()
|
||||
onIndexChanged: {
|
||||
if (index !== -1)
|
||||
idx = index;
|
||||
function forEachPopup(fn: var): void {
|
||||
for (const n of [...card.notifs])
|
||||
fn(n);
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: removeAnim
|
||||
|
||||
PropertyAction {
|
||||
property: "ListView.delayRemove"
|
||||
target: wrapper
|
||||
value: true
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "enabled"
|
||||
target: wrapper
|
||||
value: false
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "implicitHeight"
|
||||
target: wrapper
|
||||
value: 0
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "z"
|
||||
target: wrapper
|
||||
value: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "x"
|
||||
target: notif
|
||||
to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "ListView.delayRemove"
|
||||
target: wrapper
|
||||
value: false
|
||||
}
|
||||
function holdPopups(): void {
|
||||
forEachPopup(n => n.holdPopup());
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
LazyListView.trackViewport: !expanded && card.nonAnimHeight < card.implicitHeight
|
||||
LazyListView.preferredHeight: LazyListView.removing ? 0 : card.nonAnimHeight
|
||||
LazyListView.visibleHeight: LazyListView.removing ? 0 : card.implicitHeight
|
||||
LazyListView.onRemovingChanged: {
|
||||
if (!wrapper.LazyListView.removing)
|
||||
return;
|
||||
|
||||
slideOut.to = (area.x >= 0 ? root.implicitWidth : -root.implicitWidth) * 2;
|
||||
slideOut.start();
|
||||
}
|
||||
|
||||
implicitWidth: area.implicitWidth
|
||||
implicitHeight: card.implicitHeight
|
||||
|
||||
enabled: !LazyListView.removing
|
||||
z: LazyListView.removing ? 1 : 0
|
||||
|
||||
onHoldingChanged: {
|
||||
if (holding)
|
||||
holdPopups();
|
||||
else
|
||||
forEachPopup(n => n.releasePopup());
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onNotifsChanged(): void {
|
||||
if (wrapper.holding)
|
||||
wrapper.holdPopups();
|
||||
}
|
||||
|
||||
target: wrapper.modelData
|
||||
}
|
||||
|
||||
Anim {
|
||||
id: slideOut
|
||||
|
||||
target: area
|
||||
property: "x"
|
||||
duration: Tokens.anim.durations.normal
|
||||
easing: Tokens.anim.emphasized
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: wrapper.idx === 0 ? 0 : 8
|
||||
color: "transparent"
|
||||
implicitHeight: notif.implicitHeight
|
||||
implicitWidth: notif.implicitWidth
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
Notification {
|
||||
id: notif
|
||||
implicitWidth: area.implicitWidth
|
||||
implicitHeight: card.implicitHeight
|
||||
clip: true
|
||||
|
||||
NotifSwipeArea {
|
||||
id: area
|
||||
|
||||
implicitWidth: root.implicitWidth - root.padding * 2
|
||||
modelData: wrapper.modelData
|
||||
implicitHeight: card.implicitHeight
|
||||
x: wrapper.LazyListView.adding ? implicitWidth : 0
|
||||
|
||||
expanded: wrapper.expanded
|
||||
|
||||
onRequestExpand: expand => wrapper.expanded = expand
|
||||
onSwiped: wrapper.forEachPopup(n => n.dismissPopup())
|
||||
onDismissed: wrapper.forEachPopup(n => n.dismiss())
|
||||
onClicked: event => {
|
||||
if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton || card.notifs.length !== 1)
|
||||
return;
|
||||
|
||||
const actions = card.notifs[0].actions;
|
||||
if (actions.length === 1)
|
||||
actions[0].invoke();
|
||||
}
|
||||
|
||||
NotifGroupCard {
|
||||
id: card
|
||||
|
||||
group: wrapper.modelData
|
||||
container: view
|
||||
expanded: wrapper.expanded
|
||||
onRequestExpand: expand => wrapper.expanded = expand
|
||||
onSwiped: notif => notif.dismissPopup()
|
||||
onLinkActivated: notif => notif.dismissPopup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
enabled: wrapper.LazyListView.ready
|
||||
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,515 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import ZShell.Components
|
||||
import ZShell.Services
|
||||
import qs.Helpers
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
property bool expanded: Config.notifs.openExpanded
|
||||
readonly property bool hasAppIcon: modelData.appIcon.length > 0
|
||||
readonly property bool hasImage: modelData.image.length > 0
|
||||
required property NotificationRow modelData
|
||||
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Tokens.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
|
||||
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondaryContainer : Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: inner.implicitHeight
|
||||
radius: Tokens.rounding.large - Tokens.padding.smaller
|
||||
x: implicitWidth
|
||||
|
||||
Behavior on x {
|
||||
Anim {
|
||||
easing: Tokens.anim.emphasizedDecel
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
x = 0;
|
||||
modelData.lock(this);
|
||||
}
|
||||
Component.onDestruction: modelData.unlock(this)
|
||||
|
||||
MouseArea {
|
||||
property int startY
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
anchors.fill: parent
|
||||
cursorShape: root.expanded && body.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
||||
drag.axis: Drag.XAxis
|
||||
drag.target: parent
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
|
||||
onClicked: event => {
|
||||
if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton)
|
||||
return;
|
||||
|
||||
const actions = root.modelData.actions;
|
||||
if (actions.length === 1)
|
||||
actions[0].invoke();
|
||||
}
|
||||
onEntered: root.modelData.pausePopup()
|
||||
onExited: {
|
||||
if (!pressed)
|
||||
root.modelData.resumePopup();
|
||||
}
|
||||
onPositionChanged: event => {
|
||||
if (pressed) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
root.expanded = diffY > 0;
|
||||
}
|
||||
}
|
||||
onPressed: event => {
|
||||
root.modelData.pausePopup();
|
||||
startY = event.y;
|
||||
if (event.button === Qt.MiddleButton)
|
||||
root.modelData.close();
|
||||
}
|
||||
onReleased: event => {
|
||||
if (!containsMouse)
|
||||
root.modelData.resumePopup();
|
||||
|
||||
if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold)
|
||||
root.x = 0;
|
||||
else
|
||||
root.modelData.popup = false;
|
||||
}
|
||||
|
||||
Item {
|
||||
id: inner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
implicitHeight: root.nonAnimHeight
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: image
|
||||
|
||||
active: root.hasImage
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
asynchronous: true
|
||||
height: Config.notifs.sizes.image
|
||||
visible: root.hasImage || root.hasAppIcon
|
||||
width: Config.notifs.sizes.image
|
||||
|
||||
sourceComponent: ClipRect {
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) : Colors.palette.m3secondaryContainer
|
||||
implicitHeight: Config.notifs.sizes.image
|
||||
implicitWidth: Config.notifs.sizes.image
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: Qt.resolvedUrl(root.modelData.image)
|
||||
sourceSize: {
|
||||
const size = Config.notifs.sizes.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
|
||||
return Qt.size(size, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: appIcon
|
||||
|
||||
active: root.hasAppIcon || !root.hasImage
|
||||
anchors.bottom: root.hasImage ? image.bottom : undefined
|
||||
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
|
||||
anchors.right: root.hasImage ? image.right : undefined
|
||||
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) : Colors.palette.m3secondaryContainer
|
||||
implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
|
||||
implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Loader {
|
||||
id: icon
|
||||
|
||||
active: root.hasAppIcon
|
||||
anchors.centerIn: parent
|
||||
asynchronous: true
|
||||
height: Math.round(parent.width * 0.6)
|
||||
width: Math.round(parent.width * 0.6)
|
||||
|
||||
sourceComponent: ColoredIcon {
|
||||
anchors.fill: parent
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
layer.enabled: root.modelData.appIcon.endsWith("symbolic")
|
||||
source: Quickshell.iconPath(root.modelData.appIcon)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: !root.hasAppIcon
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: 1
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
|
||||
fontStyle: Tokens.font.icon.medium
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: progressIndicator
|
||||
|
||||
anchors.centerIn: appIcon
|
||||
height: appIcon.implicitHeight + progressShape.strokeWidth * 2
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
width: appIcon.implicitWidth + progressShape.strokeWidth * 2
|
||||
|
||||
ShapePath {
|
||||
id: progressShape
|
||||
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
strokeColor: Colors.palette.m3primary
|
||||
strokeWidth: 2
|
||||
|
||||
PathAngleArc {
|
||||
id: progressArc
|
||||
|
||||
centerX: progressIndicator.width / 2
|
||||
centerY: progressIndicator.height / 2
|
||||
radiusX: progressIndicator.width / 2 - Tokens.padding.extraSmall / 2
|
||||
radiusY: progressIndicator.height / 2 - Tokens.padding.extraSmall / 2
|
||||
startAngle: -90
|
||||
sweepAngle: (root.modelData.progress / 100) * 360
|
||||
|
||||
Behavior on sweepAngle {
|
||||
Anim {
|
||||
easing: Tokens.anim.emphasizedDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: appName
|
||||
|
||||
anchors.left: image.right
|
||||
anchors.leftMargin: Tokens.spacing.small
|
||||
anchors.top: parent.top
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
maximumLineCount: 1
|
||||
opacity: root.expanded ? 1 : 0
|
||||
text: appNameMetrics.elidedText
|
||||
font: Tokens.font.label.medium
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: appNameMetrics
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Tokens.spacing.small * 3
|
||||
font: appName.font
|
||||
text: root.modelData.appName
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: summary
|
||||
|
||||
anchors.left: image.right
|
||||
anchors.leftMargin: Tokens.spacing.small
|
||||
anchors.top: parent.top
|
||||
animate: true
|
||||
height: implicitHeight
|
||||
maximumLineCount: 1
|
||||
text: summaryMetrics.elidedText
|
||||
|
||||
Behavior on height {
|
||||
Anim {}
|
||||
}
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.expanded
|
||||
|
||||
PropertyChanges {
|
||||
body.anchors.topMargin: Tokens.spacing.extraSmall
|
||||
bodyPreview.anchors.topMargin: Tokens.spacing.extraSmall
|
||||
summary.anchors.topMargin: Tokens.spacing.extraSmall
|
||||
summary.maximumLineCount: undefined
|
||||
}
|
||||
|
||||
AnchorChanges {
|
||||
anchors.top: appName.bottom
|
||||
target: summary
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
PropertyAction {
|
||||
property: "maximumLineCount"
|
||||
target: summary
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "topMargin"
|
||||
}
|
||||
|
||||
AnchorAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: summaryMetrics
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Tokens.spacing.small * 3
|
||||
font: summary.font
|
||||
text: root.modelData.summary
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: timeSep
|
||||
|
||||
anchors.left: summary.right
|
||||
anchors.leftMargin: Tokens.spacing.small
|
||||
anchors.top: parent.top
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: "•"
|
||||
font: Tokens.font.body.small
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.expanded
|
||||
|
||||
AnchorChanges {
|
||||
anchors.left: appName.right
|
||||
target: timeSep
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
AnchorAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: time
|
||||
|
||||
anchors.left: timeSep.right
|
||||
anchors.leftMargin: Tokens.spacing.small
|
||||
anchors.top: parent.top
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.body.small
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: root.modelData.timeStr
|
||||
}
|
||||
|
||||
Item {
|
||||
id: expandBtn
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: -Tokens.padding.extraSmall
|
||||
implicitHeight: expandIcon.implicitHeight
|
||||
implicitWidth: expandIcon.implicitHeight
|
||||
|
||||
StateLayer {
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
onClicked: root.expanded = !root.expanded
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: expandIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: root.expanded ? -1 : 1
|
||||
rotation: root.expanded ? 180 : 0
|
||||
text: "expand_more"
|
||||
fontStyle: Tokens.font.icon.medium
|
||||
|
||||
Behavior on anchors.verticalCenterOffset {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on rotation {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: bodyPreview
|
||||
|
||||
anchors.left: summary.left
|
||||
anchors.right: expandBtn.left
|
||||
anchors.rightMargin: Tokens.spacing.small
|
||||
anchors.top: summary.bottom
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.body.small
|
||||
opacity: root.expanded ? 0 : 1
|
||||
text: bodyPreviewMetrics.elidedText
|
||||
textFormat: Text.MarkdownText
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: bodyPreviewMetrics
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: bodyPreview.width
|
||||
font: bodyPreview.font
|
||||
text: root.modelData.body
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: body
|
||||
|
||||
anchors.left: summary.left
|
||||
anchors.right: expandBtn.left
|
||||
anchors.rightMargin: Tokens.spacing.small
|
||||
anchors.top: summary.bottom
|
||||
animate: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
height: text ? implicitHeight : 0
|
||||
font: Tokens.font.body.medium
|
||||
opacity: root.expanded ? 1 : 0
|
||||
text: root.modelData.body
|
||||
textFormat: Text.MarkdownText
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
onLinkActivated: link => {
|
||||
if (!root.expanded)
|
||||
return;
|
||||
|
||||
Quickshell.execDetached(["app2unit", "-O", "--", link]);
|
||||
root.modelData.popup = false;
|
||||
}
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
id: actions
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: body.bottom
|
||||
anchors.topMargin: Tokens.spacing.small
|
||||
opacity: root.expanded ? 1 : 0
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: root.expanded
|
||||
fillWidth: root.modelData.actions.length === 0
|
||||
icon: "close"
|
||||
font: Tokens.font.icon.builders.small.size(14).build()
|
||||
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
||||
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
|
||||
isRound: true
|
||||
padding: Tokens.padding.extraSmall
|
||||
shapeMorph: true
|
||||
|
||||
onClicked: root.modelData.close()
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.modelData.actions
|
||||
|
||||
TextButton {
|
||||
required property var modelData
|
||||
|
||||
enabled: root.expanded
|
||||
fillWidth: true
|
||||
implicitWidth: label.implicitWidth
|
||||
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
||||
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
|
||||
isRound: true
|
||||
label.anchors.centerIn: undefined
|
||||
label.anchors.left: left
|
||||
label.anchors.margins: Tokens.padding.small
|
||||
label.anchors.right: right
|
||||
label.anchors.verticalCenter: verticalCenter
|
||||
label.elide: Text.ElideRight
|
||||
label.horizontalAlignment: Text.AlignHCenter
|
||||
shapeMorph: true
|
||||
text: modelData.text
|
||||
|
||||
onClicked: {
|
||||
if (modelData.invokable)
|
||||
modelData.invoke();
|
||||
else if (!root.modelData.resident)
|
||||
root.modelData.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: root.expanded
|
||||
fillWidth: root.modelData.actions.length === 0
|
||||
icon: copyTimer.running ? "inventory" : "content_copy"
|
||||
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
||||
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
|
||||
isRound: true
|
||||
label.animate: true
|
||||
padding: Tokens.padding.extraSmall
|
||||
shapeMorph: true
|
||||
font: Tokens.font.icon.builders.small.size(14).build()
|
||||
|
||||
onClicked: {
|
||||
Quickshell.clipboardText = root.modelData.body;
|
||||
copyTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyTimer
|
||||
|
||||
interval: 3000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ Item {
|
||||
property real offsetScale: shouldBeActive ? 0 : 1
|
||||
required property ShellScreen screen
|
||||
readonly property bool shouldBeActive: screenState.osd && Config.osd.enabled && !(screenState.utilities && Config.utilities.enabled)
|
||||
property real sidebarOffset: sidebarOrSessionVisible ? 12 : 0
|
||||
property real sidebarOffset: sidebarOrSessionVisible ? 0 : 0
|
||||
required property bool sidebarOrSessionVisible
|
||||
property bool sourceMuted
|
||||
property real sourceVolume
|
||||
|
||||
@@ -21,6 +21,8 @@ CustomRect {
|
||||
implicitWidth: ZUtils.snapCeil(450, root)
|
||||
radius: Tokens.rounding.largeIncreased
|
||||
|
||||
objectName: "card-rect"
|
||||
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
@@ -93,6 +95,8 @@ CustomRect {
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.largeIncreased
|
||||
|
||||
objectName: "progress-layout"
|
||||
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
orientation: Qt.Vertical
|
||||
|
||||
@@ -114,7 +118,7 @@ CustomRect {
|
||||
|
||||
CustomProgressBar {
|
||||
fgColor: root.accent
|
||||
implicitHeight: Tokens.padding.small
|
||||
implicitHeight: Tokens.padding.extraSmall
|
||||
indeterminate: isNaN(root.usage) || isNaN(root.temperature)
|
||||
value: root.temperature / 100
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ CustomRect {
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
orientation: Qt.Vertical
|
||||
id: usageColumn
|
||||
orientation: Qt.Vertical
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: Tokens.padding.extraSmall
|
||||
|
||||
@@ -51,7 +51,7 @@ CustomRect {
|
||||
id: sparkline
|
||||
|
||||
property bool initialized: false
|
||||
property real smoothMax: targetMax
|
||||
property real smoothMax: targetMax * 1.6
|
||||
property real targetMax: 1024
|
||||
|
||||
anchors.fill: parent
|
||||
@@ -74,7 +74,7 @@ CustomRect {
|
||||
Component.onCompleted: {
|
||||
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
|
||||
|
||||
sparkline.smoothMax = Qt.binding(() => sparkline.targetMax);
|
||||
sparkline.smoothMax = Qt.binding(() => sparkline.targetMax * 1.6);
|
||||
|
||||
sparkline.initialized = true;
|
||||
}
|
||||
|
||||
@@ -28,8 +28,7 @@ Item {
|
||||
|
||||
Behavior on offsetScale {
|
||||
Anim {
|
||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
easing: Tokens.anim.expressiveDefaultSpatial
|
||||
type: Anim.DefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import QtQuick.Layouts
|
||||
import QtQuick
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Modules.Settings.Common
|
||||
@@ -9,9 +9,11 @@ PageBase {
|
||||
title: qsTr("Panels")
|
||||
|
||||
SnapLayout {
|
||||
orientation: Qt.Vertical
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
|
||||
orientation: Qt.Vertical
|
||||
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
width: root.cappedWidth
|
||||
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Daemons
|
||||
import ZShell.Components
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property CustomText body: (expandedContent.item as ExpandedBody)?.body ?? null
|
||||
required property bool expanded
|
||||
required property NotificationRow modelData
|
||||
readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.medium * 2 : summaryHeightMetrics.height
|
||||
required property ScreenState screenState
|
||||
|
||||
color: {
|
||||
const c = root.modelData?.urgency === "critical" ? Colors.palette.m3secondaryContainer : Colors.layer(Colors.palette.m3surfaceContainerHigh, 2);
|
||||
return expanded ? c : Qt.alpha(c, 0);
|
||||
}
|
||||
implicitHeight: nonAnimHeight
|
||||
radius: Tokens.rounding.large - Tokens.padding.small
|
||||
state: expanded ? "expanded" : ""
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
states: State {
|
||||
name: "expanded"
|
||||
|
||||
PropertyChanges {
|
||||
compactBody.anchors.margins: Tokens.padding.medium
|
||||
dummySummary.anchors.margins: Tokens.padding.medium
|
||||
expandedContent.anchors.margins: Tokens.padding.medium
|
||||
summary.anchors.margins: Tokens.padding.medium
|
||||
summary.maximumLineCount: Number.MAX_SAFE_INTEGER
|
||||
summary.width: root.width - Tokens.padding.medium * 2 - timeStr.implicitWidth - Tokens.spacing.small
|
||||
timeStr.anchors.margins: Tokens.padding.medium
|
||||
}
|
||||
}
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
properties: "margins,width,maximumLineCount"
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: summaryHeightMetrics
|
||||
|
||||
font: summary.font
|
||||
text: " " // Use this height to prevent weird characters from changing the line height
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: summary
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: root.modelData?.urgency === "critical" ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
text: root.modelData?.summary ?? ""
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: dummySummary
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
text: root.modelData?.summary ?? ""
|
||||
visible: false
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
id: compactBody
|
||||
|
||||
anchors.left: dummySummary.right
|
||||
anchors.leftMargin: Tokens.spacing.small
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
shouldBeActive: !root.expanded
|
||||
|
||||
sourceComponent: CustomText {
|
||||
color: root.modelData?.urgency === "critical" ? Colors.palette.m3secondary : Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
text: String(root.modelData?.body ?? "").replace(/\n/g, " ")
|
||||
}
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
id: timeStr
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
shouldBeActive: root.expanded
|
||||
|
||||
sourceComponent: CustomText {
|
||||
animate: true
|
||||
color: Colors.palette.m3outline
|
||||
text: root.modelData?.timeStr ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
WrappedLoader {
|
||||
id: expandedContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: summary.bottom
|
||||
anchors.topMargin: Tokens.spacing.extraSmall
|
||||
shouldBeActive: root.expanded
|
||||
|
||||
sourceComponent: ExpandedBody {}
|
||||
}
|
||||
|
||||
component ExpandedBody: SnapLayout {
|
||||
readonly property alias body: bodyText
|
||||
|
||||
spacing: Tokens.spacing.medium
|
||||
orientation: Qt.Vertical
|
||||
|
||||
CustomText {
|
||||
id: bodyText
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: root.modelData?.urgency === "critical" ? Colors.palette.m3secondary : Colors.palette.m3onSurface
|
||||
text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given")
|
||||
textFormat: Text.MarkdownText
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
onLinkActivated: link => {
|
||||
Qt.openUrlExternally(link);
|
||||
root.screenState.sidebar = false;
|
||||
}
|
||||
}
|
||||
|
||||
NotifActionList {
|
||||
notif: root.modelData
|
||||
}
|
||||
}
|
||||
component WrappedLoader: Loader {
|
||||
id: comp
|
||||
|
||||
required property bool shouldBeActive
|
||||
|
||||
active: false
|
||||
opacity: 0
|
||||
|
||||
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
|
||||
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"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "active"
|
||||
to: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import ZShell.Config
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
import qs.Effects
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property NotificationRow notif
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: actionList.implicitHeight
|
||||
|
||||
SnapLayout {
|
||||
id: actionList
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
isClose: true
|
||||
},
|
||||
...Array.from(root.notif?.actions ?? []),
|
||||
{
|
||||
isCopy: true
|
||||
}
|
||||
]
|
||||
|
||||
CustomRect {
|
||||
id: action
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Tokens.padding.large : 0)
|
||||
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 4)
|
||||
implicitHeight: actionInner.implicitHeight + Tokens.padding.small
|
||||
implicitWidth: actionInner.implicitWidth + Tokens.padding.medium * 2
|
||||
radius: actionStateLayer.pressed ? Tokens.rounding.medium / 2 : Tokens.rounding.large
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
type: Anim.FastSpatial
|
||||
}
|
||||
}
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
type: Anim.FastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyTimer
|
||||
|
||||
interval: 3000
|
||||
|
||||
onTriggered: actionInner.item.text = "content_copy"
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: actionStateLayer
|
||||
|
||||
onClicked: {
|
||||
if (action.modelData.isClose) {
|
||||
root.notif.close();
|
||||
} else if (action.modelData.isCopy) {
|
||||
Quickshell.clipboardText = root.notif.body;
|
||||
actionInner.item.text = "inventory";
|
||||
copyTimer.start();
|
||||
} else if (action.modelData.invoke) {
|
||||
action.modelData.invoke();
|
||||
} else if (!root.notif.resident) {
|
||||
root.notif.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: actionInner
|
||||
|
||||
anchors.centerIn: parent
|
||||
sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif?.hasActionIcons ? iconComp : textComp
|
||||
}
|
||||
|
||||
Component {
|
||||
id: iconBtn
|
||||
|
||||
MaterialIcon {
|
||||
animate: action.modelData.isCopy ?? false
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: action.modelData.isCopy ? "content_copy" : "close"
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: iconComp
|
||||
|
||||
IconImage {
|
||||
asynchronous: true
|
||||
source: Quickshell.iconPath(action.modelData.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textComp
|
||||
|
||||
CustomText {
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: action.modelData.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,88 +7,101 @@ import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import qs.Daemons
|
||||
import qs.Modules
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property int notifCount: NotifServer.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
|
||||
required property ScreenState screenState
|
||||
readonly property int notifCount: NotifServer.notClosed.length
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.small
|
||||
|
||||
Component.onCompleted: NotifServer.list.forEach(n => n.popup = false)
|
||||
Component.onCompleted: NotifServer.list.forEach(n => n.dismissPopup())
|
||||
|
||||
Item {
|
||||
id: title
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: 4
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Tokens.padding.extraSmall
|
||||
|
||||
implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight)
|
||||
|
||||
CustomText {
|
||||
id: count
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.notifCount > 0 ? 0 : -width - titleText.anchors.leftMargin
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: root.notifCount > 0 ? 1 : 0
|
||||
|
||||
text: root.notifCount
|
||||
color: Colors.palette.m3outline
|
||||
font: Tokens.font.label.large
|
||||
opacity: root.notifCount > 0 ? 1 : 0
|
||||
text: root.notifCount
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: titleText
|
||||
|
||||
anchors.left: count.right
|
||||
anchors.leftMargin: 7
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: Tokens.font.label.large
|
||||
anchors.left: count.right
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: Tokens.spacing.extraSmall
|
||||
|
||||
text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications")
|
||||
color: Colors.palette.m3outline
|
||||
font: Tokens.font.label.large
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: clipRect
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: title.bottom
|
||||
anchors.topMargin: 10
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: Tokens.spacing.medium
|
||||
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
asynchronous: true
|
||||
anchors.centerIn: parent
|
||||
active: opacity > 0
|
||||
opacity: root.notifCount > 0 ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: 20
|
||||
sourceComponent: SnapLayout {
|
||||
orientation: Qt.Vertical
|
||||
spacing: Tokens.spacing.extraLarge
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("No notifications")
|
||||
color: Colors.palette.m3outlineVariant
|
||||
font: Tokens.font.headline.builders.small.width(90).build()
|
||||
text: qsTr("No Notifications")
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.StandardExtraLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,10 +110,10 @@ Item {
|
||||
id: view
|
||||
|
||||
anchors.fill: parent
|
||||
contentHeight: notifList.implicitHeight
|
||||
contentWidth: width
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: notifList.implicitHeight
|
||||
|
||||
CustomScrollBar.vertical: CustomScrollBar {
|
||||
flickable: view
|
||||
@@ -109,8 +122,8 @@ Item {
|
||||
NotifDockList {
|
||||
id: notifList
|
||||
|
||||
container: view
|
||||
screenState: root.screenState
|
||||
container: view
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,21 +131,19 @@ Item {
|
||||
Timer {
|
||||
id: clearTimer
|
||||
|
||||
interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(NotifServer.notClosed.length)))
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
|
||||
interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(NotifServer.notClosed.length)))
|
||||
onTriggered: {
|
||||
const first = NotifServer.notClosed[0];
|
||||
if (!first) {
|
||||
const group = NotifServer.groups[0];
|
||||
if (!group) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const appName = first.appName;
|
||||
let cleared = 0;
|
||||
for (const n of NotifServer.notClosed.filter(n => n.appName === appName)) {
|
||||
n.close();
|
||||
for (const n of [...group.activeNotifs]) {
|
||||
n.dismiss();
|
||||
cleared++;
|
||||
if (cleared > 30) {
|
||||
interval = 5;
|
||||
@@ -143,33 +154,40 @@ Item {
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Tokens.padding.small
|
||||
asynchronous: true
|
||||
anchors.right: parent.right
|
||||
opacity: root.notifCount > 0 ? 1 : 0
|
||||
scale: root.notifCount > 0 ? 1 : 0.5
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
scale: root.notifCount > 0 ? 1 : 0.5
|
||||
opacity: root.notifCount > 0 ? 1 : 0
|
||||
active: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
sourceComponent: IconButton {
|
||||
id: clearBtn
|
||||
|
||||
font: Tokens.font.icon.large
|
||||
icon: "clear_all"
|
||||
font: Tokens.font.icon.large
|
||||
isRound: true
|
||||
|
||||
onClicked: clearTimer.start()
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: clearBtn.stateLayer.containsMouse ? 4 : 3
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
level: clearBtn.stateLayer.containsMouse ? 4 : 3
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
type: Anim.FastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+119
-115
@@ -4,8 +4,10 @@ import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Daemons
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Components.Notifs
|
||||
import qs.Daemons
|
||||
import qs.Services
|
||||
|
||||
LazyListView {
|
||||
@@ -16,121 +18,12 @@ LazyListView {
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
asynchronous: true
|
||||
cacheBuffer: 400
|
||||
implicitHeight: contentHeight
|
||||
readyDelay: 1
|
||||
removeDuration: Tokens.anim.durations.normal
|
||||
|
||||
spacing: Tokens.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
|
||||
property int startY
|
||||
|
||||
function closeAll(): void {
|
||||
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
|
||||
drag.target: this
|
||||
enabled: !closed
|
||||
hoverEnabled: true
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1
|
||||
preventStealing: true
|
||||
scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: notif.LazyListView.ready
|
||||
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: event => {
|
||||
if (pressed) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
notifInner.toggleExpand(diffY > 0);
|
||||
}
|
||||
}
|
||||
onPressed: event => {
|
||||
startY = event.y;
|
||||
if (event.button === Qt.RightButton)
|
||||
notifInner.toggleExpand(!notifInner.expanded);
|
||||
else if (event.button === Qt.MiddleButton)
|
||||
closeAll();
|
||||
}
|
||||
onReleased: event => {
|
||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
closeAll();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: clearTimer
|
||||
|
||||
interval: 15
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
const notifs = NotifServer.notClosed.filter(n => n.appName === notif.modelData);
|
||||
if (notifs.length === 0) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const n of notifs.slice(0, 30))
|
||||
n.close();
|
||||
}
|
||||
}
|
||||
|
||||
NotifGroup {
|
||||
id: notifInner
|
||||
|
||||
container: root.container
|
||||
modelData: notif.modelData
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
}
|
||||
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()];
|
||||
}
|
||||
}
|
||||
readyDelay: 1
|
||||
cacheBuffer: 400
|
||||
asynchronous: true
|
||||
|
||||
onViewportAdjustNeeded: d => {
|
||||
if (contentYAnim.running)
|
||||
@@ -139,10 +32,121 @@ LazyListView {
|
||||
contentYAnim.start();
|
||||
}
|
||||
|
||||
useCustomViewport: true
|
||||
viewport: Qt.rect(0, container.contentY, width, container.height)
|
||||
|
||||
removeDuration: Tokens.anim.durations.normal
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...NotifServer.groups]
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
NotifSwipeArea {
|
||||
id: notif
|
||||
|
||||
required property int index
|
||||
required property NotificationGroup modelData
|
||||
|
||||
readonly property bool closed: card.notifs.length === 0
|
||||
|
||||
function toggleExpand(expand: bool): void {
|
||||
if (expand) {
|
||||
if (!expanded)
|
||||
root.screenState.expandedNotifs.push(modelData.appName);
|
||||
} else if (expanded) {
|
||||
root.screenState.expandedNotifs.splice(root.screenState.expandedNotifs.indexOf(modelData.appName), 1);
|
||||
}
|
||||
}
|
||||
|
||||
function closeAll(): void {
|
||||
clearTimer.start();
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (closed && expanded)
|
||||
toggleExpand(false);
|
||||
}
|
||||
|
||||
LazyListView.trackViewport: !expanded && card.nonAnimHeight < card.implicitHeight
|
||||
LazyListView.preferredHeight: closed ? 0 : card.nonAnimHeight
|
||||
LazyListView.visibleHeight: card.implicitHeight
|
||||
implicitHeight: card.implicitHeight
|
||||
|
||||
opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1
|
||||
scale: LazyListView.adding ? 0 : 1
|
||||
|
||||
expanded: root.screenState.expandedNotifs.includes(modelData.appName)
|
||||
enabled: !closed
|
||||
|
||||
onClosedChanged: {
|
||||
if (closed)
|
||||
slideOut.start();
|
||||
}
|
||||
onRequestExpand: expand => toggleExpand(expand)
|
||||
onSwiped: closeAll()
|
||||
onDismissed: closeAll()
|
||||
|
||||
Anim {
|
||||
id: slideOut
|
||||
|
||||
target: notif
|
||||
property: "x"
|
||||
to: notif.x >= 0 ? notif.width : -notif.width
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: clearTimer
|
||||
|
||||
interval: 15
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
const notifs = [...notif.modelData.activeNotifs];
|
||||
if (notifs.length === 0) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const n of notifs.slice(0, 30))
|
||||
n.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
NotifGroupCard {
|
||||
id: card
|
||||
|
||||
group: notif.modelData
|
||||
container: root.container
|
||||
expanded: notif.expanded
|
||||
baseLayer: 1
|
||||
onRequestExpand: expand => notif.toggleExpand(expand)
|
||||
onSwiped: notif => notif.dismiss()
|
||||
onLinkActivated: root.screenState.sidebar = false
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
enabled: notif.LazyListView.ready
|
||||
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
id: contentYAnim
|
||||
|
||||
property: "contentY"
|
||||
target: root.container
|
||||
property: "contentY"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
import qs.Helpers
|
||||
import qs.Daemons
|
||||
import ZShell.Components
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property list<var> activeNotifs: notifs.filter(n => !n.closed)
|
||||
readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
|
||||
required property Flickable container
|
||||
readonly property bool expanded: screenState.expandedNotifs.includes(modelData)
|
||||
readonly property string image: activeNotifs.find(n => n.image.length > 0)?.image ?? ""
|
||||
required property string modelData
|
||||
readonly property int nonAnimHeight: {
|
||||
const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0);
|
||||
const columnHeight = headerHeight + notifList.layoutHeight;
|
||||
return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Tokens.padding.small * 2);
|
||||
}
|
||||
readonly property int notifCount: activeNotifs.length
|
||||
readonly property list<var> notifs: NotifServer.list.filter(n => n.appName === modelData)
|
||||
required property ScreenState screenState
|
||||
readonly property int urgency: {
|
||||
if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical))
|
||||
return NotificationUrgency.Critical;
|
||||
if (activeNotifs.find(n => n.urgency === NotificationUrgency.Normal))
|
||||
return NotificationUrgency.Normal;
|
||||
return NotificationUrgency.Low;
|
||||
}
|
||||
|
||||
function toggleExpand(expand: bool): void {
|
||||
if (expand) {
|
||||
if (!expanded)
|
||||
screenState.expandedNotifs.push(modelData);
|
||||
} else if (expanded) {
|
||||
screenState.expandedNotifs.splice(screenState.expandedNotifs.indexOf(modelData), 1);
|
||||
}
|
||||
}
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
clip: true
|
||||
color: Colors.layer(Colors.palette.m3surfaceContainer, 2)
|
||||
implicitHeight: nonAnimHeight
|
||||
radius: Tokens.rounding.large
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (notifCount === 0 && expanded)
|
||||
screenState.expandedNotifs.splice(screenState.expandedNotifs.indexOf(modelData), 1);
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
id: content
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
objectName: "notif-content"
|
||||
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
implicitHeight: Config.notifs.sizes.image
|
||||
implicitWidth: Config.notifs.sizes.image
|
||||
|
||||
Component {
|
||||
id: imageComp
|
||||
|
||||
Image {
|
||||
asynchronous: true
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
height: Config.notifs.sizes.image
|
||||
source: Qt.resolvedUrl(root.image)
|
||||
sourceSize: {
|
||||
const size = Config.notifs.sizes.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
|
||||
return Qt.size(size, size);
|
||||
}
|
||||
width: Config.notifs.sizes.image
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appIconComp
|
||||
|
||||
ColoredIcon {
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
implicitSize: Math.round(Config.notifs.sizes.image * 0.6)
|
||||
layer.enabled: root.appIcon.endsWith("symbolic")
|
||||
source: Quickshell.iconPath(root.appIcon)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: materialIconComp
|
||||
|
||||
MaterialIcon {
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency)
|
||||
}
|
||||
}
|
||||
|
||||
ClipRect {
|
||||
anchors.fill: parent
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHigh, 3) : Colors.palette.m3secondaryContainer
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
Loader {
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: sourceComponent === materialIconComp ? 1 : 0
|
||||
asynchronous: true
|
||||
sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.appIcon && root.image
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.palette.m3surfaceContainerHigh : Colors.palette.m3secondaryContainer
|
||||
implicitHeight: Config.notifs.sizes.badge
|
||||
implicitWidth: Config.notifs.sizes.badge
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
ColoredIcon {
|
||||
anchors.centerIn: parent
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
||||
implicitSize: Math.round(Config.notifs.sizes.badge * 0.6)
|
||||
layer.enabled: root.appIcon.endsWith("symbolic")
|
||||
source: Quickshell.iconPath(root.appIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
id: column
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0
|
||||
orientation: Qt.Vertical
|
||||
|
||||
objectName: "notif-column"
|
||||
|
||||
Behavior on spacing {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
id: header
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
objectName: "notif-header"
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
text: root.modelData
|
||||
}
|
||||
|
||||
CustomText {
|
||||
animate: true
|
||||
color: Colors.palette.m3outline
|
||||
text: root.activeNotifs[0]?.timeStr ?? ""
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : Colors.layer(Colors.palette.m3surfaceContainerHigh, 3)
|
||||
implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall
|
||||
implicitWidth: expandBtn.implicitWidth + Tokens.padding.large
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
StateLayer {
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurface
|
||||
|
||||
onClicked: root.toggleExpand(!root.expanded)
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
id: expandBtn
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
objectName: "notif-expand"
|
||||
|
||||
CustomText {
|
||||
id: groupCount
|
||||
|
||||
Layout.leftMargin: Tokens.padding.extraSmall / 2
|
||||
animate: true
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurfaceVariant
|
||||
text: root.notifCount
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.rightMargin: -Tokens.padding.extraSmall / 2
|
||||
Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.extraSmall) : 0
|
||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurfaceVariant
|
||||
rotation: root.expanded ? 180 : 0
|
||||
text: "expand_more"
|
||||
|
||||
Behavior on Layout.topMargin {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on rotation {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NotifGroupList {
|
||||
id: notifList
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
container: root.container
|
||||
expanded: root.expanded
|
||||
notifs: root.notifs
|
||||
screenState: root.screenState
|
||||
|
||||
onRequestToggleExpand: expand => root.toggleExpand(expand)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import ZShell.Services
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
LazyListView {
|
||||
id: root
|
||||
|
||||
required property Flickable container
|
||||
required property bool expanded
|
||||
required property list<var> notifs
|
||||
required property ScreenState screenState
|
||||
|
||||
signal requestToggleExpand(expand: bool)
|
||||
|
||||
asynchronous: true
|
||||
cacheBuffer: 400
|
||||
implicitHeight: contentHeight
|
||||
readyDelay: 1
|
||||
removeDuration: Tokens.anim.durations.normal
|
||||
spacing: Math.round(Tokens.spacing.extraSmall)
|
||||
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);
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
MouseArea {
|
||||
id: notif
|
||||
|
||||
required property int index
|
||||
required property NotificationRow 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 ?? true)
|
||||
hoverEnabled: true
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
opacity: LazyListView.removing || LazyListView.adding ? 0 : 1
|
||||
preventStealing: !root.expanded
|
||||
scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {}
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: notif.LazyListView.ready
|
||||
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Component.onCompleted: modelData?.lock(this)
|
||||
Component.onDestruction: modelData?.unlock(this)
|
||||
onPositionChanged: event => {
|
||||
if (pressed && !root.expanded) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
root.requestToggleExpand(diffY > 0);
|
||||
}
|
||||
}
|
||||
onPressed: event => {
|
||||
startY = event.y;
|
||||
if (event.button === Qt.RightButton)
|
||||
root.requestToggleExpand(!root.expanded);
|
||||
else if (event.button === Qt.MiddleButton)
|
||||
modelData?.close();
|
||||
}
|
||||
onReleased: event => {
|
||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
modelData?.close();
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: notif.modelData?.closed ?? false
|
||||
|
||||
onFinished: notif.modelData?.unlock(notif)
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
target: notif
|
||||
to: 0
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "x"
|
||||
target: notif
|
||||
to: notif.x >= 0 ? notif.width : -notif.width
|
||||
}
|
||||
}
|
||||
|
||||
Notif {
|
||||
id: notifInner
|
||||
|
||||
anchors.fill: parent
|
||||
expanded: root.expanded
|
||||
modelData: notif.modelData
|
||||
screenState: root.screenState
|
||||
}
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
if (root.expanded)
|
||||
return root.notifs as Array;
|
||||
|
||||
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) as Array;
|
||||
}
|
||||
}
|
||||
|
||||
TransformWatcher {
|
||||
id: tWatcher
|
||||
|
||||
a: root.container.contentItem
|
||||
b: root
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user