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 import Quickshell.Io
JsonObject { JsonObject {
property int maxNotifs: 5
property JsonObject sizes: JsonObject { property JsonObject sizes: JsonObject {
property int border: 100 property int border: 100
property int clockWidth: 800 property int clockWidth: 800

View file

@ -12,6 +12,8 @@ Item {
required property real weatherWidth required property real weatherWidth
required property real buttonsWidth required property real buttonsWidth
required property real buttonsHeight required property real buttonsHeight
required property real statusWidth
required property real statusHeight
required property bool isNormal required property bool isNormal
required property bool isLarge required property bool isLarge
@ -21,6 +23,8 @@ Item {
readonly property real weatherRight: innerMask.anchors.margins + weatherPath.width readonly property real weatherRight: innerMask.anchors.margins + weatherPath.width
readonly property real buttonsTop: innerMask.anchors.margins + buttonsPath.height readonly property real buttonsTop: innerMask.anchors.margins + buttonsPath.height
readonly property real buttonsLeft: innerMask.anchors.margins + buttonsPath.width 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 mediaX: innerMask.anchors.margins + mediaPath.width
readonly property real mediaY: innerMask.anchors.margins + mediaPath.height 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 { component Anim: NumberAnimation {

View file

@ -75,6 +75,8 @@ WlSessionLockSurface {
weatherWidth: weather.implicitWidth weatherWidth: weather.implicitWidth
buttonsWidth: buttons.item?.nonAnimWidth ?? 0 buttonsWidth: buttons.item?.nonAnimWidth ?? 0
buttonsHeight: buttons.item?.nonAnimHeight ?? 0 buttonsHeight: buttons.item?.nonAnimHeight ?? 0
statusWidth: status.item?.nonAnimWidth ?? 0
statusHeight: status.item?.nonAnimHeight ?? 0
isNormal: root.screen.width > Config.lock.sizes.smallScreenWidth isNormal: root.screen.width > Config.lock.sizes.smallScreenWidth
isLarge: root.screen.width > Config.lock.sizes.largeScreenWidth isLarge: root.screen.width > Config.lock.sizes.largeScreenWidth
visible: false visible: false
@ -168,6 +170,19 @@ WlSessionLockSurface {
sourceComponent: Buttons {} 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 { component Anim: NumberAnimation {
duration: Appearance.anim.durations.large duration: Appearance.anim.durations.large
easing.type: Easing.BezierSpline 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
}
}