Rework main page UX:
- Get rid of cards in favour of a more plain list view - Implement removing (deleting) accountsmaster
parent
a0caf83da2
commit
6ae5f7be21
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,95 +48,34 @@ Kirigami.ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: mainListDelegate
|
||||||
|
AccountEntryView {
|
||||||
|
account: model.account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: mainPageComponent
|
id: mainPageComponent
|
||||||
Kirigami.ScrollablePage {
|
Kirigami.ScrollablePage {
|
||||||
title: i18n("OTP")
|
title: i18nc("@title:window", "Accounts")
|
||||||
actions.main: addAction
|
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
|
model: accounts
|
||||||
delegate: Kirigami.AbstractCard {
|
delegate: Kirigami.DelegateRecycler {
|
||||||
contentItem: Item {
|
width: parent ? parent.width : implicitWidth
|
||||||
implicitWidth: delegateLayout.implicitWidth
|
sourceComponent: mainListDelegate
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: addPageComponent
|
id: addPageComponent
|
||||||
Kirigami.Page {
|
Kirigami.Page {
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file alias="main.qml">contents/ui/main.qml</file>
|
<file alias="main.qml">contents/ui/main.qml</file>
|
||||||
<file alias="TokenDetailsForm.qml">contents/ui/TokenDetailsForm.qml</file>
|
<file alias="TokenDetailsForm.qml">contents/ui/TokenDetailsForm.qml</file>
|
||||||
|
<file alias="AccountEntryView.qml">contents/ui/AccountEntryView.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
Loading…
Reference in New Issue