Rework main page UX:

- Get rid of cards in favour of a more plain list view
 - Implement removing (deleting) accounts
master
Johan Ouwerkerk 2019-12-27 19:04:07 +01:00
parent a0caf83da2
commit 6ae5f7be21
3 changed files with 135 additions and 80 deletions

View File

@ -0,0 +1,115 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
import QtQuick 2.1
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.0 as Controls
import org.kde.kirigami 2.4 as Kirigami
import Keysmith.Models 1.0 as Models
Kirigami.SwipeListItem {
id: root
property Models.Account account: null
property int phase : account && account.isTotp ? account.millisecondsLeftForToken() : 0
property int interval: account && account.isTotp ? 1000 * account.timeStep : 0
property real healthIndicator: 0
property Kirigami.Action advanceCounter : Kirigami.Action {
iconName: "go-next" // "view-refresh"
text: "Next token"
onTriggered: {
// TODO convert to C++ helper, have proper logging?
if (account && account.isHotp) {
account.advanceCounter(1);
}
// TODO warn if not
}
}
property Kirigami.Action deleteAccount : Kirigami.Action {
iconName: "edit-delete"
text: "Delete account"
onTriggered: {
// TODO convert to C++ helper, have proper logging?
if (account) {
account.remove();
}
// TODO warn if not
}
}
actions: account && account.isHotp ? [deleteAccount, advanceCounter] : [deleteAccount]
contentItem: ColumnLayout {
id: mainLayout
RowLayout {
Controls.Label {
id: accountNameLabel
horizontalAlignment: Text.AlignLeft
font.weight: Font.Light
elide: Text.ElideRight
text: account ? account.name : i18nc("placeholder text if no account name is available", "(untitled)")
}
Controls.Label {
id: tokenValueLabel
horizontalAlignment: Text.AlignRight
Layout.fillWidth: true
font.weight: Font.Bold
text: account && account.token && account.token.length > 0 ? account.token : i18nc("placeholder text if no token is available", "(refresh)")
}
}
Timer {
id: timer
running: account && account.isTotp
interval: phase
onTriggered: {
// TODO convert to C++ helper, have proper logging?
if (account) {
if (account.isTotp) {
timer.stop()
timeoutIndicatorAnimation.stop();
account.recompute();
timer.interval = account.millisecondsLeftForToken(); // root.interval;
timer.restart();
timeoutIndicatorAnimation.restart();
}
}
// TODO warn if not
}
}
Rectangle {
id: health
Layout.fillWidth: true
color: Kirigami.Theme.positiveTextColor
height: Kirigami.Units.smallSpacing
opacity: timer.running ? 0.6 : 0
radius: health.height
/*
* Don't use mainLayout.width because that doesn't seem to be affected by hovering which uncovers 'hidden'
* action buttons. Compute the correct width manually, to avoid 'flashes' where the health indicator may
* appear to be 'reset' to (near) full width.
*/
width: (accountNameLabel.width + tokenValueLabel.width) * healthIndicator
NumberAnimation {
id: timeoutIndicatorAnimation
/*
* Don't animate the rectangle directly: instead animate a proxy property to track the desired ratio.
* This way the property binding for the width of the rectangle is fully re-evaluated whenever the
* app window size changes. In turn, that then ensures the health indicator rectangle is also resized
* accordingly to the correct proportion of the new width of the layout.
*/
target: root
property: "healthIndicator"
from: timer.interval / root.interval
to: 0
duration: timer.interval
running: model.account && model.account.isTotp && units.longDuration > 1
}
}
}
}

View File

@ -48,95 +48,34 @@ Kirigami.ApplicationWindow {
}
}
Component {
id: mainListDelegate
AccountEntryView {
account: model.account
}
}
Component {
id: mainPageComponent
Kirigami.ScrollablePage {
title: i18n("OTP")
title: i18nc("@title:window", "Accounts")
actions.main: addAction
Kirigami.CardsListView {
id: view
/*
* Explicitly opt-out of scroll-to-refresh/drag-to-refresh behaviour
* Underlying model implementations don't offer the hooks for that.
*/
supportsRefreshing: false
ListView {
id: mainList
model: accounts
delegate: Kirigami.AbstractCard {
contentItem: Item {
implicitWidth: delegateLayout.implicitWidth
implicitHeight: delegateLayout.implicitHeight
GridLayout {
id: delegateLayout
anchors {
left: parent.left
top: parent.top
right: parent.right
//IMPORTANT: never put the bottom margin
}
rowSpacing: Kirigami.Units.largeSpacing
columnSpacing: Kirigami.Units.largeSpacing
columns: width > Kirigami.Units.gridUnit * 20 ? 4 : 2
ColumnLayout {
Controls.Label {
Layout.fillWidth: true
text: model.account ? model.account.name : i18nc("placeholder text if no account name is available", "(untitled)")
}
Kirigami.Heading {
level: 2
text: model.account && model.account.token && model.account.token.length > 0 ? model.account.token : i18nc("placeholder text if no token is available", "(refresh)")
}
}
Controls.Button {
Layout.alignment: Qt.AlignRight|Qt.AlignVCenter
Layout.columnSpan: 2
text: i18nc("%1 is current counter numerical value", "Refresh (%1)", model.counter)
visible: model.account && model.account.isHotp
onClicked: {
if(model.account) {
model.account.advanceCounter();
}
}
}
Timer {
id: timeoutTimer
repeat: false
interval: model.account && model.account.isTotp ? model.account.millisecondsLeftForToken() : 0
running: model.account && model.account.isTotp
onTriggered: {
if (model.account) {
model.account.recompute();
timeoutTimer.stop();
timeoutIndicatorAnimation.stop();
timeoutTimer.interval = model.account.millisecondsLeftForToken();
timeoutTimer.restart();
timeoutIndicatorAnimation.restart();
}
}
}
Rectangle {
id: timeoutIndicatorRect
Layout.fillHeight: true
width: 5
radius: width
color: "green"
visible: timeoutTimer.running && units.longDuration > 1
opacity: timeoutIndicatorAnimation.running ? 0.6 : 0
Behavior on opacity {
NumberAnimation {
duration: units.longDuration
}
}
}
NumberAnimation {
id: timeoutIndicatorAnimation
target: timeoutIndicatorRect
property: "height"
from: delegateLayout.height
to: 0
duration: timeoutTimer.interval
running: model.account && model.account.isTotp && units.longDuration > 1
}
}
}
delegate: Kirigami.DelegateRecycler {
width: parent ? parent.width : implicitWidth
sourceComponent: mainListDelegate
}
}
}
}
Component {
id: addPageComponent
Kirigami.Page {

View File

@ -2,5 +2,6 @@
<qresource prefix="/">
<file alias="main.qml">contents/ui/main.qml</file>
<file alias="TokenDetailsForm.qml">contents/ui/TokenDetailsForm.qml</file>
<file alias="AccountEntryView.qml">contents/ui/AccountEntryView.qml</file>
</qresource>
</RCC>