summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-06-25 20:52:15 +1000
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-06-25 20:52:15 +1000
commitd7d4d86da2952e44642c65502aaaefaa69c6d948 (patch)
tree28ae984386060dcf81394932fe365c63de4793c5 /modules
parentinternal: move notif icon guessing to utils (diff)
downloadcaelestia-shell-d7d4d86da2952e44642c65502aaaefaa69c6d948.tar.gz
caelestia-shell-d7d4d86da2952e44642c65502aaaefaa69c6d948.tar.bz2
caelestia-shell-d7d4d86da2952e44642c65502aaaefaa69c6d948.zip
lock: add notifs and status
Diffstat (limited to 'modules')
-rw-r--r--modules/lock/Backgrounds.qml70
-rw-r--r--modules/lock/LockSurface.qml15
-rw-r--r--modules/lock/Notification.qml220
-rw-r--r--modules/lock/Status.qml249
4 files changed, 554 insertions, 0 deletions
diff --git a/modules/lock/Backgrounds.qml b/modules/lock/Backgrounds.qml
index e561c66..b02d2a3 100644
--- a/modules/lock/Backgrounds.qml
+++ b/modules/lock/Backgrounds.qml
@@ -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 {
diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml
index 047116f..83fb89f 100644
--- a/modules/lock/LockSurface.qml
+++ b/modules/lock/LockSurface.qml
@@ -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
diff --git a/modules/lock/Notification.qml b/modules/lock/Notification.qml
new file mode 100644
index 0000000..1d3e1c7
--- /dev/null
+++ b/modules/lock/Notification.qml
@@ -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
+ }
+ }
+}
diff --git a/modules/lock/Status.qml b/modules/lock/Status.qml
new file mode 100644
index 0000000..e11fa25
--- /dev/null
+++ b/modules/lock/Status.qml
@@ -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
+ }
+}