lockscreen: Refactor and lazy load notifications

This commit is contained in:
Devin Lin 2022-12-09 10:54:02 -05:00
parent c4472ca39a
commit 2a779900f3
5 changed files with 310 additions and 294 deletions

View file

@ -12,80 +12,75 @@ import org.kde.plasma.private.mobileshell 1.0 as MobileShell
import org.kde.notificationmanager 1.0 as NotificationManager import org.kde.notificationmanager 1.0 as NotificationManager
Loader { Item {
id: root id: root
required property real openFactor required property real openFactor
required property real statusBarHeight
readonly property real statusBarHeight: PlasmaCore.Units.gridUnit * 1.25
property var notificationsModel: [] property var notificationsModel: []
signal passwordRequested() signal passwordRequested()
asynchronous: true // top status bar
MobileShell.StatusBar {
sourceComponent: Item { id: statusBar
// top status bar
MobileShell.StatusBar { anchors.top: parent.top
id: statusBar anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.left: parent.left height: root.statusBarHeight
anchors.right: parent.right
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
height: root.statusBarHeight backgroundColor: "transparent"
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup showSecondRow: false
backgroundColor: "transparent" showDropShadow: true
showTime: false
showSecondRow: false disableSystemTray: true // prevent SIGABRT, since loading the system tray on the lockscreen leads to bad... things
showDropShadow: true }
showTime: false
disableSystemTray: true // prevent SIGABRT, since loading the system tray on the lockscreen leads to bad... things // drag down gesture to open action drawer
MobileShell.ActionDrawerOpenSurface {
id: swipeArea
actionDrawer: drawer
anchors.fill: statusBar
}
// action drawer component
MobileShell.ActionDrawer {
id: drawer
anchors.fill: parent
visible: offset !== 0
restrictedPermissions: true
notificationSettings: NotificationManager.Settings {}
notificationModel: root.notificationsModel
notificationModelType: MobileShell.NotificationsModelType.WatchedNotificationsModel
property bool requestNotificationAction: false
// notification button clicked, requesting auth
onPermissionsRequested: {
requestNotificationAction = true;
drawer.close();
root.passwordRequested();
} }
}
// drag down gesture to open action drawer
MobileShell.ActionDrawerOpenSurface { // listen to authentication events
id: swipeArea Connections {
actionDrawer: drawer target: authenticator
anchors.fill: statusBar function onSucceeded() {
} // run pending action if successfully unlocked
if (drawer.requestNotificationAction) {
// action drawer component drawer.runPendingAction();
MobileShell.ActionDrawer {
id: drawer
anchors.fill: parent
restrictedPermissions: true
notificationSettings: NotificationManager.Settings {}
notificationModel: root.notificationsModel
notificationModelType: MobileShell.NotificationsModelType.WatchedNotificationsModel
property bool requestNotificationAction: false
// notification button clicked, requesting auth
onPermissionsRequested: {
requestNotificationAction = true;
drawer.close();
root.passwordRequested();
}
}
// listen to authentication events
Connections {
target: authenticator
function onSucceeded() {
// run pending action if successfully unlocked
if (drawer.requestNotificationAction) {
drawer.runPendingAction();
drawer.requestNotificationAction = false;
}
}
function onFailed() {
drawer.requestNotificationAction = false; drawer.requestNotificationAction = false;
} }
} }
function onFailed() {
drawer.requestNotificationAction = false;
}
} }
} }

View file

@ -1,7 +1,7 @@
/* /*
* SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de> * SPDX-FileCopyrightText: 2019 Nicolas Fella <nicolas.fella@gmx.de>
* SPDX-FileCopyrightText: 2021-2022 Devin Lin <espidev@gmail.com> * SPDX-FileCopyrightText: 2021-2022 Devin Lin <espidev@gmail.com>
* *
* SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
@ -17,25 +17,22 @@ import org.kde.kirigami 2.12 as Kirigami
/** /**
* Lockscreen component that is loaded after the device is locked. * Lockscreen component that is loaded after the device is locked.
* *
* Special attention must be paid to ensuring the GUI loads as fast as possible. * Special attention must be paid to ensuring the GUI loads as fast as possible.
*/ */
PlasmaCore.ColorScope { Item {
id: root id: root
property var lockScreenState: LockScreenState {} property var lockScreenState: LockScreenState {}
property var notifModel: Notifications.WatchedNotificationsModel {} property var notifModel: Notifications.WatchedNotificationsModel {}
// only show widescreen mode for short height devices (ex. phone landscape) // only show widescreen mode for short height devices (ex. phone landscape)
property bool isWidescreen: root.height < 720 && (root.height < root.width * 0.75) property bool isWidescreen: root.height < 720 && (root.height < root.width * 0.75)
property bool notificationsShown: false property bool notificationsShown: false
readonly property bool drawerOpen: flickable.openFactor >= 1 readonly property bool drawerOpen: flickable.openFactor >= 1
property var passwordBar: keypadLoader.item.passwordBar property var passwordBar: keypadLoader.item.passwordBar
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
anchors.fill: parent
// listen for keyboard events, and focus on input area // listen for keyboard events, and focus on input area
Component.onCompleted: forceActiveFocus(); Component.onCompleted: forceActiveFocus();
Keys.onPressed: { Keys.onPressed: {
@ -43,7 +40,7 @@ PlasmaCore.ColorScope {
flickable.goToOpenPosition(); flickable.goToOpenPosition();
passwordBar.textField.forceActiveFocus(); passwordBar.textField.forceActiveFocus();
} }
// HACK: kscreenlocker doesn't draw the wallpaper (shows up as black) // HACK: kscreenlocker doesn't draw the wallpaper (shows up as black)
Loader { Loader {
anchors.fill: parent anchors.fill: parent
@ -55,8 +52,8 @@ PlasmaCore.ColorScope {
anchors.fill: parent anchors.fill: parent
} }
} }
// wallpaper blur // wallpaper blur
Loader { Loader {
anchors.fill: parent anchors.fill: parent
asynchronous: true asynchronous: true
@ -65,181 +62,192 @@ PlasmaCore.ColorScope {
blur: root.notificationsShown || root.drawerOpen // only blur once animation finished for performance blur: root.notificationsShown || root.drawerOpen // only blur once animation finished for performance
} }
} }
// header bar and action drawer
HeaderComponent {
id: headerBar
z: 1 // on top of flick area
anchors.fill: parent
openFactor: flickable.openFactor
notificationsModel: root.notifModel
onPasswordRequested: root.askPassword()
}
Connections { Connections {
target: root.lockScreenState target: root.lockScreenState
// ensure keypad is opened when password is updated (ex. keyboard) // ensure keypad is opened when password is updated (ex. keyboard)
function onPasswordChanged() { function onPasswordChanged() {
flickable.goToOpenPosition() flickable.goToOpenPosition()
} }
} }
FlickContainer { PlasmaCore.ColorScope {
id: flickable
anchors.fill: parent anchors.fill: parent
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
property real openFactor: position / keypadHeight
// header bar and action drawer
onOpened: { Loader {
if (root.lockScreenState.passwordless) { id: headerBarLoader
// try unlocking if flicked to the top, and we have passwordless login z: 1 // on top of flick area
root.lockScreenState.tryPassword(); readonly property real statusBarHeight: PlasmaCore.Units.gridUnit * 1.25
}
} anchors.fill: parent
asynchronous: true
keypadHeight: PlasmaCore.Units.gridUnit * 20
sourceComponent: HeaderComponent {
// go to closed position when loaded statusBarHeight: headerBarLoader.statusBarHeight
Component.onCompleted: { openFactor: flickable.openFactor
flickable.position = 0;
flickable.goToClosePosition();
}
// update position, and cap it at the keypad height
onPositionChanged: {
if (position > keypadHeight) {
position = keypadHeight;
} else if (position < 0) {
position = 0;
}
}
Item {
width: flickable.width
height: flickable.height
y: flickable.contentY // effectively anchored to the screen
LockScreenNarrowContent {
id: phoneComponent
visible: !isWidescreen
active: visible
opacity: 1 - flickable.openFactor
fullHeight: root.height
lockScreenState: root.lockScreenState
notificationsModel: root.notifModel notificationsModel: root.notifModel
onNotificationsShownChanged: root.notificationsShown = notificationsShown onPasswordRequested: root.askPassword()
onPasswordRequested: flickable.goToOpenPosition()
anchors.top: parent.top
anchors.bottom: scrollUpIconLoader.top
anchors.left: parent.left
anchors.right: parent.right
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
} }
}
LockScreenWideScreenContent {
id: tabletComponent FlickContainer {
id: flickable
visible: isWidescreen anchors.fill: parent
active: visible
opacity: 1 - flickable.openFactor property real openFactor: position / keypadHeight
lockScreenState: root.lockScreenState onOpened: {
notificationsModel: root.notifModel if (root.lockScreenState.passwordless) {
onNotificationsShownChanged: root.notificationsShown = notificationsShown // try unlocking if flicked to the top, and we have passwordless login
root.lockScreenState.tryPassword();
onPasswordRequested: flickable.goToOpenPosition()
anchors.topMargin: headerBar.statusBarHeight
anchors.top: parent.top
anchors.bottom: scrollUpIconLoader.top
anchors.left: parent.left
anchors.right: parent.right
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
}
// scroll up icon
Loader {
id: scrollUpIconLoader
asynchronous: true
anchors.bottom: parent.bottom
anchors.bottomMargin: PlasmaCore.Units.gridUnit + flickable.position * 0.5
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: PlasmaCore.IconItem {
id: scrollUpIcon
implicitWidth: PlasmaCore.Units.iconSizes.smallMedium
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
opacity: 1 - flickable.openFactor
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
source: "arrow-up"
} }
} }
// password keypad keypadHeight: PlasmaCore.Units.gridUnit * 20
Loader {
id: keypadLoader // go to closed position when loaded
width: parent.width Component.onCompleted: {
asynchronous: true flickable.position = 0;
active: !root.lockScreenState.passwordless // only load keypad if not passwordless flickable.goToClosePosition();
}
anchors.bottom: parent.bottom
// update position, and cap it at the keypad height
sourceComponent: ColumnLayout { onPositionChanged: {
property alias passwordBar: keypad.passwordBar if (position > keypadHeight) {
position = keypadHeight;
transform: Translate { y: flickable.keypadHeight - flickable.position } } else if (position < 0) {
spacing: 0 position = 0;
}
// info notification text }
Label {
Layout.fillWidth: true Item {
Layout.rightMargin: Kirigami.Units.largeSpacing width: flickable.width
Layout.leftMargin: Kirigami.Units.largeSpacing height: flickable.height
Layout.bottomMargin: PlasmaCore.Units.smallSpacing * 2 y: flickable.contentY // effectively anchored to the screen
font.pointSize: 9
LockScreenNarrowContent {
elide: Text.ElideRight id: phoneComponent
horizontalAlignment: Text.AlignHCenter
text: root.lockScreenState.info visible: !isWidescreen
opacity: (root.lockScreenState.info.length === 0 || flickable.openFactor < 1) ? 0 : 1 active: visible
color: 'white' opacity: 1 - flickable.openFactor
Behavior on opacity { fullHeight: root.height
NumberAnimation { duration: 200 }
} lockScreenState: root.lockScreenState
} notificationsModel: root.notifModel
onNotificationsShownChanged: root.notificationsShown = notificationsShown
// scroll down icon
PlasmaCore.IconItem { onPasswordRequested: flickable.goToOpenPosition()
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: PlasmaCore.Units.gridUnit anchors.top: parent.top
anchors.bottom: scrollUpIconLoader.top
anchors.left: parent.left
anchors.right: parent.right
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
}
LockScreenWideScreenContent {
id: tabletComponent
visible: isWidescreen
active: visible
opacity: 1 - flickable.openFactor
lockScreenState: root.lockScreenState
notificationsModel: root.notifModel
onNotificationsShownChanged: root.notificationsShown = notificationsShown
onPasswordRequested: flickable.goToOpenPosition()
anchors.topMargin: headerBarLoader.statusBarHeight
anchors.top: parent.top
anchors.bottom: scrollUpIconLoader.top
anchors.left: parent.left
anchors.right: parent.right
// move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) }
}
// scroll up icon
Loader {
id: scrollUpIconLoader
asynchronous: true
anchors.bottom: parent.bottom
anchors.bottomMargin: PlasmaCore.Units.gridUnit + flickable.position * 0.5
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: PlasmaCore.IconItem {
id: scrollUpIcon
implicitWidth: PlasmaCore.Units.iconSizes.smallMedium implicitWidth: PlasmaCore.Units.iconSizes.smallMedium
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup opacity: 1 - flickable.openFactor
source: "arrow-down"
opacity: Math.sin((Math.PI / 2) * flickable.openFactor + 1.5 * Math.PI) + 1
}
Keypad { colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
id: keypad source: "arrow-up"
Layout.fillWidth: true }
focus: true }
lockScreenState: root.lockScreenState // password keypad
swipeProgress: flickable.openFactor Loader {
id: keypadLoader
width: parent.width
asynchronous: true
active: !root.lockScreenState.passwordless // only load keypad if not passwordless
anchors.bottom: parent.bottom
sourceComponent: ColumnLayout {
property alias passwordBar: keypad.passwordBar
transform: Translate { y: flickable.keypadHeight - flickable.position }
spacing: 0
// info notification text
Label {
Layout.fillWidth: true
Layout.rightMargin: Kirigami.Units.largeSpacing
Layout.leftMargin: Kirigami.Units.largeSpacing
Layout.bottomMargin: PlasmaCore.Units.smallSpacing * 2
font.pointSize: 9
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: root.lockScreenState.info
opacity: (root.lockScreenState.info.length === 0 || flickable.openFactor < 1) ? 0 : 1
color: 'white'
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
// scroll down icon
PlasmaCore.IconItem {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: PlasmaCore.Units.gridUnit
implicitWidth: PlasmaCore.Units.iconSizes.smallMedium
implicitHeight: PlasmaCore.Units.iconSizes.smallMedium
colorGroup: PlasmaCore.Theme.ComplementaryColorGroup
source: "arrow-down"
opacity: Math.sin((Math.PI / 2) * flickable.openFactor + 1.5 * Math.PI) + 1
}
Keypad {
id: keypad
Layout.fillWidth: true
focus: true
lockScreenState: root.lockScreenState
swipeProgress: flickable.openFactor
}
} }
} }
} }

View file

@ -13,51 +13,51 @@ import org.kde.plasma.private.mobileshell 1.0 as MobileShell
Loader { Loader {
id: root id: root
required property var lockScreenState
property var notificationsModel: []
required property var lockScreenState
property var notificationsModel: []
property bool notificationsShown: false property bool notificationsShown: false
property real fullHeight property real fullHeight
signal passwordRequested() signal passwordRequested()
// avoid topMargin animation when item is being loaded // avoid topMargin animation when item is being loaded
onLoaded: loadTimer.restart(); onLoaded: loadTimer.restart();
Timer { Timer {
id: loadTimer id: loadTimer
interval: PlasmaCore.Units.longDuration interval: 200
} }
// move while swiping up // move while swiping up
transform: Translate { y: Math.round((1 - phoneComponent.opacity) * (-root.height / 6)) } transform: Translate { y: Math.round((1 - root.opacity) * (-root.height / 6)) }
asynchronous: true asynchronous: true
sourceComponent: Item { sourceComponent: Item {
ColumnLayout { ColumnLayout {
id: column id: column
spacing: 0 spacing: 0
// center clock when no notifications are shown, otherwise move the clock upward // center clock when no notifications are shown, otherwise move the clock upward
anchors.topMargin: !root.notificationsShown ? Math.round(root.fullHeight / 2 - (column.implicitHeight / 2)) : PlasmaCore.Units.gridUnit * 5 anchors.topMargin: !root.notificationsShown ? Math.round(root.fullHeight / 2 - (column.implicitHeight / 2)) : PlasmaCore.Units.gridUnit * 5
anchors.bottomMargin: PlasmaCore.Units.gridUnit anchors.bottomMargin: PlasmaCore.Units.gridUnit
anchors.fill: parent anchors.fill: parent
// animate // animate
Behavior on anchors.topMargin { Behavior on anchors.topMargin {
NumberAnimation { NumberAnimation {
duration: loadTimer.running ? 0 : PlasmaCore.Units.longDuration duration: loadTimer.running ? 0 : PlasmaCore.Units.veryLongDuration
easing.type: Easing.InOutQuad easing.type: Easing.InOutExpo
} }
} }
Clock { Clock {
layoutAlignment: Qt.AlignHCenter layoutAlignment: Qt.AlignHCenter
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: PlasmaCore.Units.gridUnit * 2 // keep spacing even if media controls are gone Layout.bottomMargin: PlasmaCore.Units.gridUnit * 2 // keep spacing even if media controls are gone
} }
MobileShell.MediaControlsWidget { MobileShell.MediaControlsWidget {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
@ -65,18 +65,18 @@ Loader {
Layout.leftMargin: PlasmaCore.Units.gridUnit Layout.leftMargin: PlasmaCore.Units.gridUnit
Layout.rightMargin: PlasmaCore.Units.gridUnit Layout.rightMargin: PlasmaCore.Units.gridUnit
} }
NotificationsComponent { NotificationsComponent {
id: notificationComponent id: notificationComponent
lockScreenState: root.lockScreenState lockScreenState: root.lockScreenState
notificationsModel: root.notificationsModel notificationsModel: root.notificationsModel
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: PlasmaCore.Units.gridUnit * (25 + 2) // clip margins Layout.maximumWidth: PlasmaCore.Units.gridUnit * (25 + 2) // clip margins
topMargin: PlasmaCore.Units.gridUnit topMargin: PlasmaCore.Units.gridUnit
onPasswordRequested: root.passwordRequested() onPasswordRequested: root.passwordRequested()
onNotificationsShownChanged: root.notificationsShown = notificationsShown onNotificationsShownChanged: root.notificationsShown = notificationsShown
} }

View file

@ -15,8 +15,8 @@ Loader {
id: root id: root
required property var lockScreenState required property var lockScreenState
property var notificationsModel: []
property var notificationsModel: []
property bool notificationsShown: false property bool notificationsShown: false
signal passwordRequested() signal passwordRequested()

View file

@ -1,6 +1,6 @@
/* /*
* SPDX-FileCopyrightText: 2022 Devin Lin <espidev@gmail.com> * SPDX-FileCopyrightText: 2022 Devin Lin <espidev@gmail.com>
* *
* SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
@ -14,27 +14,34 @@ import org.kde.plasma.private.mobileshell 1.0 as MobileShell
import org.kde.notificationmanager 1.0 as NotificationManager import org.kde.notificationmanager 1.0 as NotificationManager
Rectangle { Loader {
id: root id: root
required property var lockScreenState required property var lockScreenState
property var notificationsModel: [] property var notificationsModel: []
property var notificationSettings: NotificationManager.Settings {} property var notificationSettings: NotificationManager.Settings {}
readonly property bool notificationsShown: notificationsList.hasNotifications
signal passwordRequested()
property real leftMargin: 0 property real leftMargin: 0
property real rightMargin: 0 property real rightMargin: 0
property real topMargin: 0 property real topMargin: 0
property real bottomMargin: 0 property real bottomMargin: 0
readonly property bool notificationsShown: item && item.notificationsList.hasNotifications
color: "transparent"
clip: true property var notificationsList: item ? item.notificationsList : null
signal passwordRequested()
// perform delayed loading of notifications
active: false
Timer {
interval: 500
running: true
onTriggered: root.active = true
}
Connections { Connections {
target: lockScreenState target: lockScreenState
function onUnlockSucceeded() { function onUnlockSucceeded() {
// run pending action if successfully unlocked // run pending action if successfully unlocked
if (notificationsList.requestNotificationAction) { if (notificationsList.requestNotificationAction) {
@ -42,34 +49,40 @@ Rectangle {
notificationsList.requestNotificationAction = false; notificationsList.requestNotificationAction = false;
} }
} }
function onUnlockFailed() { function onUnlockFailed() {
notificationsList.requestNotificationAction = false; notificationsList.requestNotificationAction = false;
} }
} }
PlasmaCore.ColorScope { sourceComponent: Item {
anchors.fill: parent clip: true
anchors.topMargin: root.topMargin
anchors.bottomMargin: root.bottomMargin property alias notificationsList: notificationsList
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin PlasmaCore.ColorScope {
colorGroup: PlasmaCore.Theme.NormalColorGroup
MobileShell.NotificationsWidget {
id: notificationsList
anchors.fill: parent anchors.fill: parent
anchors.topMargin: root.topMargin
historyModelType: MobileShell.NotificationsModelType.WatchedNotificationsModel anchors.bottomMargin: root.bottomMargin
actionsRequireUnlock: true anchors.leftMargin: root.leftMargin
historyModel: root.notificationsModel anchors.rightMargin: root.rightMargin
notificationSettings: root.notificationSettings colorGroup: PlasmaCore.Theme.NormalColorGroup
property bool requestNotificationAction: false MobileShell.NotificationsWidget {
id: notificationsList
onUnlockRequested: { anchors.fill: parent
requestNotificationAction = true;
root.passwordRequested(); historyModelType: MobileShell.NotificationsModelType.WatchedNotificationsModel
actionsRequireUnlock: true
historyModel: root.notificationsModel
notificationSettings: root.notificationSettings
property bool requestNotificationAction: false
onUnlockRequested: {
requestNotificationAction = true;
root.passwordRequested();
}
} }
} }
} }