lock: add notifs and status
This commit is contained in:
parent
fbb58e3e1d
commit
d7d4d86da2
5 changed files with 556 additions and 0 deletions
|
|
@ -1,6 +1,8 @@
|
|||
import Quickshell.Io
|
||||
|
||||
JsonObject {
|
||||
property int maxNotifs: 5
|
||||
|
||||
property JsonObject sizes: JsonObject {
|
||||
property int border: 100
|
||||
property int clockWidth: 800
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
220
modules/lock/Notification.qml
Normal file
220
modules/lock/Notification.qml
Normal 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
249
modules/lock/Status.qml
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue