lock: add notifs and status

This commit is contained in:
2 * r + 2 * t 2025-06-25 20:52:15 +10:00
parent fbb58e3e1d
commit d7d4d86da2
5 changed files with 556 additions and 0 deletions

View file

@ -1,6 +1,8 @@
import Quickshell.Io
JsonObject {
property int maxNotifs: 5
property JsonObject sizes: JsonObject {
property int border: 100
property int clockWidth: 800

View file

@ -12,6 +12,8 @@ Item {
required property real weatherWidth
required property real buttonsWidth
required property real buttonsHeight
required property real statusWidth
required property real statusHeight
required property bool isNormal
required property bool isLarge
@ -21,6 +23,8 @@ Item {
readonly property real weatherRight: innerMask.anchors.margins + weatherPath.width
readonly property real buttonsTop: innerMask.anchors.margins + buttonsPath.height
readonly property real buttonsLeft: innerMask.anchors.margins + buttonsPath.width
readonly property real statusBottom: innerMask.anchors.margins + statusPath.height
readonly property real statusLeft: innerMask.anchors.margins + statusPath.width
readonly property real mediaX: innerMask.anchors.margins + mediaPath.width
readonly property real mediaY: innerMask.anchors.margins + mediaPath.height
@ -400,6 +404,72 @@ Item {
}
}
}
ShapePath {
id: statusPath
property int width: root.locked ? root.statusWidth - Config.lock.sizes.border / 4 : 0
property real height: root.locked ? root.statusHeight - Config.lock.sizes.border / 4 : 0
readonly property real rounding: Appearance.rounding.large * 2
readonly property real roundingX: width < rounding * 2 ? width / 2 : rounding
readonly property real roundingY: height < rounding * 2 ? height / 2 : rounding
strokeWidth: -1
fillColor: root.isLarge ? Config.border.colour : "transparent"
startX: Math.ceil(innerMask.width)
startY: height + roundingY
PathArc {
relativeX: -statusPath.roundingX
relativeY: -statusPath.roundingY
radiusX: Math.min(statusPath.rounding, statusPath.width)
radiusY: Math.min(statusPath.rounding, statusPath.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: -(statusPath.width - statusPath.roundingX * 2)
relativeY: 0
}
PathArc {
relativeX: -statusPath.roundingX
relativeY: -statusPath.roundingY
radiusX: Math.min(statusPath.rounding, statusPath.width)
radiusY: Math.min(statusPath.rounding, statusPath.height)
}
PathLine {
relativeX: 0
relativeY: -(statusPath.height - statusPath.roundingY * 2)
}
PathArc {
relativeX: -statusPath.roundingX
relativeY: -statusPath.roundingY
radiusX: Math.min(statusPath.rounding, statusPath.width)
radiusY: Math.min(statusPath.rounding, statusPath.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: statusPath.width + statusPath.roundingX
relativeY: 0
}
Behavior on width {
Anim {}
}
Behavior on height {
Anim {}
}
Behavior on fillColor {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
}
component Anim: NumberAnimation {

View file

@ -75,6 +75,8 @@ WlSessionLockSurface {
weatherWidth: weather.implicitWidth
buttonsWidth: buttons.item?.nonAnimWidth ?? 0
buttonsHeight: buttons.item?.nonAnimHeight ?? 0
statusWidth: status.item?.nonAnimWidth ?? 0
statusHeight: status.item?.nonAnimHeight ?? 0
isNormal: root.screen.width > Config.lock.sizes.smallScreenWidth
isLarge: root.screen.width > Config.lock.sizes.largeScreenWidth
visible: false
@ -168,6 +170,19 @@ WlSessionLockSurface {
sourceComponent: Buttons {}
}
Loader {
id: status
active: root.screen.width > Config.lock.sizes.largeScreenWidth
anchors.bottom: parent.top
anchors.left: parent.right
anchors.bottomMargin: -backgrounds.statusBottom
anchors.leftMargin: -backgrounds.statusLeft
sourceComponent: Status {}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline

View file

@ -0,0 +1,220 @@
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/services"
import "root:/config"
import "root:/utils"
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import QtQuick
import QtQuick.Layouts
StyledRect {
id: root
required property Notifs.Notif modelData
readonly property bool hasImage: modelData.image.length > 0
readonly property bool hasAppIcon: modelData.appIcon.length > 0
readonly property int nonAnimHeight: Math.max(image.height, details.implicitHeight) + Appearance.padding.normal * 2
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainer
radius: Appearance.rounding.normal
implicitWidth: Config.notifs.sizes.width
Component.onCompleted: implicitHeight = Qt.binding(() => nonAnimHeight)
Behavior on implicitHeight {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on x {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
}
RetainableLock {
object: root.modelData.notification
locked: true
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
preventStealing: true
onEntered: root.modelData.timer.stop()
onExited: root.modelData.timer.start()
drag.target: parent
drag.axis: Drag.XAxis
onPressed: event => {
if (event.button === Qt.MiddleButton)
root.modelData.notification.dismiss();
}
onReleased: event => {
if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold)
root.x = 0;
else
root.modelData.notification.dismiss(); // TODO: change back to popup when notif dock impled
}
}
Loader {
id: image
active: root.hasImage
asynchronous: true
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Appearance.padding.normal
width: Config.notifs.sizes.image
height: Config.notifs.sizes.image
visible: root.hasImage || root.hasAppIcon
sourceComponent: ClippingRectangle {
radius: Appearance.rounding.full
implicitWidth: Config.notifs.sizes.image
implicitHeight: Config.notifs.sizes.image
Image {
anchors.fill: parent
source: Qt.resolvedUrl(root.modelData.image)
fillMode: Image.PreserveAspectCrop
cache: false
asynchronous: true
}
}
}
Loader {
id: appIcon
active: root.hasAppIcon || !root.hasImage
asynchronous: true
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
anchors.right: root.hasImage ? image.right : undefined
anchors.bottom: root.hasImage ? image.bottom : undefined
sourceComponent: StyledRect {
radius: Appearance.rounding.full
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer
implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
Loader {
id: icon
active: root.hasAppIcon
asynchronous: true
anchors.centerIn: parent
visible: !root.modelData.appIcon.endsWith("symbolic")
width: Math.round(parent.width * 0.6)
height: Math.round(parent.width * 0.6)
sourceComponent: IconImage {
implicitSize: Math.round(parent.width * 0.6)
source: Quickshell.iconPath(root.modelData.appIcon)
asynchronous: true
}
}
Loader {
active: root.modelData.appIcon.endsWith("symbolic")
asynchronous: true
anchors.fill: icon
sourceComponent: Colouriser {
source: icon
colorizationColor: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer
}
}
Loader {
active: !root.hasAppIcon
asynchronous: true
anchors.centerIn: parent
anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02
anchors.verticalCenterOffset: Appearance.font.size.large * 0.02
sourceComponent: MaterialIcon {
text: Icons.getNotifIcon(root.modelData.summary.toLowerCase(), root.modelData.urgency)
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer
font.pointSize: Appearance.font.size.large
font.variableAxes: ({
opsz: Appearance.font.size.large
})
}
}
}
}
ColumnLayout {
id: details
anchors.verticalCenter: parent.verticalCenter
anchors.left: image.right
anchors.right: parent.right
anchors.leftMargin: Appearance.spacing.smaller
anchors.rightMargin: Appearance.padding.larger
spacing: 0
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
StyledText {
Layout.fillWidth: true
Layout.maximumWidth: implicitWidth
animate: true
text: root.modelData.summary
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
text: "•"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
StyledText {
animate: true
text: root.modelData.timeStr
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
}
StyledText {
Layout.fillWidth: true
animate: true
text: root.modelData.body
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
elide: Text.ElideRight
maximumLineCount: 1
}
}
}

249
modules/lock/Status.qml Normal file
View file

@ -0,0 +1,249 @@
import "root:/widgets"
import "root:/services"
import "root:/config"
import "root:/utils"
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.UPower
import QtQuick
import QtQuick.Layouts
WrapperItem {
readonly property real nonAnimWidth: (notifs.count > 0 ? Config.notifs.sizes.width : status.implicitWidth) + margin
readonly property real nonAnimHeight: {
if (notifs.count > 0) {
const count = Math.min(notifs.count, Config.lock.maxNotifs);
let height = status.implicitHeight + Appearance.spacing.normal + Appearance.spacing.smaller * (count - 1);
for (let i = 0; i < count; i++)
height += notifs.itemAtIndex(i)?.nonAnimHeight ?? 0;
return height + margin;
}
return status.implicitHeight + margin;
}
implicitWidth: nonAnimWidth
implicitHeight: nonAnimHeight
margin: Appearance.padding.large * 2
rightMargin: 0
topMargin: 0
Behavior on implicitWidth {
Anim {
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
ColumnLayout {
spacing: Appearance.spacing.normal
RowLayout {
id: status
Layout.fillWidth: true
spacing: Appearance.spacing.small
Loader {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
active: UPower.displayDevice.isLaptopBattery
asynchronous: true
sourceComponent: StyledText {
animate: true
text: qsTr("%1%2 remaining").arg(UPower.onBattery ? "" : "(+) ").arg(UPower.displayDevice.percentage)
color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? Colours.palette.m3onSurface : Colours.palette.m3error
}
}
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
animate: true
text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off"
font.pointSize: Appearance.font.size.large
}
Loader {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
Layout.maximumWidth: item?.implicitWidth ?? 0
active: !UPower.displayDevice.isLaptopBattery
asynchronous: true
sourceComponent: StyledText {
animate: true
text: Network.active?.ssid ?? ""
font.pointSize: Appearance.font.size.normal
elide: Text.ElideRight
}
}
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
animate: true
text: Bluetooth.powered ? "bluetooth" : "bluetooth_disabled"
font.pointSize: Appearance.font.size.large
}
Loader {
Layout.alignment: Qt.AlignVCenter
active: !UPower.displayDevice.isLaptopBattery
asynchronous: true
sourceComponent: StyledText {
animate: true
text: qsTr("%n device(s) connected", "", Bluetooth.devices.filter(d => d.connected).length)
font.pointSize: Appearance.font.size.normal
}
}
}
ListView {
id: notifs
Layout.fillWidth: true
Layout.fillHeight: true
model: ScriptModel {
values: [...Notifs.popups].reverse()
}
orientation: Qt.Vertical
spacing: 0
clip: true
interactive: false
delegate: Item {
id: wrapper
required property Notifs.Notif modelData
required property int index
readonly property alias nonAnimHeight: notif.nonAnimHeight
property int idx
onIndexChanged: {
if (index !== -1)
idx = index;
}
implicitWidth: notif.implicitWidth
implicitHeight: notif.nonAnimHeight + (idx === 0 ? 0 : Appearance.spacing.smaller)
ListView.onRemove: removeAnim.start()
SequentialAnimation {
id: removeAnim
PropertyAction {
target: wrapper
property: "ListView.delayRemove"
value: true
}
PropertyAction {
target: wrapper
property: "enabled"
value: false
}
PropertyAction {
target: wrapper
property: "implicitHeight"
value: 0
}
PropertyAction {
target: wrapper
property: "z"
value: 1
}
Anim {
target: notif
property: "x"
to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2
duration: Appearance.anim.durations.normal
easing.bezierCurve: Appearance.anim.curves.emphasized
}
PropertyAction {
target: wrapper
property: "ListView.delayRemove"
value: false
}
}
ClippingRectangle {
anchors.top: parent.top
anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller
color: "transparent"
radius: notif.radius
implicitWidth: notif.implicitWidth
implicitHeight: notif.nonAnimHeight
Notification {
id: notif
modelData: wrapper.modelData
}
}
}
move: Transition {
Anim {
property: "y"
}
}
displaced: Transition {
Anim {
property: "y"
}
}
StyledRect {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: Appearance.padding.normal
color: Colours.palette.m3tertiaryContainer
radius: Appearance.rounding.small
implicitWidth: count.implicitWidth + Appearance.padding.normal * 2
implicitHeight: count.implicitHeight + Appearance.padding.small * 2
scale: Notifs.popups.length > Config.lock.maxNotifs ? 1 : 0
StyledText {
id: count
anchors.centerIn: parent
text: qsTr("+%1").arg(Notifs.popups.length - Config.lock.maxNotifs)
color: Colours.palette.m3onTertiaryContainer
}
Behavior on scale {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
}
}
}
component Anim: NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}