diff --git a/src/contents/ui/AccountEntryView.qml b/src/contents/ui/AccountEntryView.qml new file mode 100644 index 0000000..fab95b5 --- /dev/null +++ b/src/contents/ui/AccountEntryView.qml @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2020 Johan Ouwerkerk + */ + +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 + } + } + } +} diff --git a/src/contents/ui/main.qml b/src/contents/ui/main.qml index c55c2be..778483f 100644 --- a/src/contents/ui/main.qml +++ b/src/contents/ui/main.qml @@ -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 { diff --git a/src/resources.qrc b/src/resources.qrc index d863868..6f6a238 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -2,5 +2,6 @@ contents/ui/main.qml contents/ui/TokenDetailsForm.qml + contents/ui/AccountEntryView.qml