yubioath-flutter/qml/CredentialCard.qml
2020-06-09 13:00:54 +02:00

423 lines
15 KiB
QML

import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQuick.Controls.Material 2.2
import QtGraphicalEffects 1.0
import "utils.js" as Utils
Pane {
id: credentialCard
property var code
property var credential
property bool touchCredentialNoCode: touchCredential && (!code
|| !code.value)
property bool hotpCredential: !!credential && (credential
&& credential.oath_type === "HOTP")
property bool hotpCredentialInCoolDown
property bool customPeriodCredentialNoTouch: !!credential && (credential.period !== 30
&& credential.oath_type === "TOTP"
&& !touchCredential)
property bool touchCredential: !!credential && credential && credential.touch
property bool favorite: !!credential ? settings.favorites.includes(credential.key) : false
property string searchQuery: toolBar.searchField.text
Layout.fillHeight: true
Layout.fillWidth: true
function toggleFavorite() {
if (favorite) {
settings.favorites = settings.favorites.filter(fav => fav !== credential.key)
} else {
let favs = settings.favorites
favs.push(credential.key)
settings.favorites = favs
}
entries.sort()
}
function formattedCode(code) {
// Add a space in the code for easier reading.
if (!!code) {
switch (code.length) {
case 6:
// 123 123
return code.slice(0, 3) + " " + code.slice(3)
case 7:
// 1234 123
return code.slice(0, 4) + " " + code.slice(4)
case 8:
// 1234 1234
return code.slice(0, 4) + " " + code.slice(4)
default:
return code
}
}
}
function formattedName() {
if (!!credential && credential.issuer) {
return credential.issuer + " (" + credential.name + ")"
} else if (!!credential) {
return credential.name
} else {
return ""
}
}
function copyCode(code) {
clipBoard.push(code)
navigator.snackBar(qsTr("Code copied to clipboard"))
}
function calculateCard(copy) {
if (touchCredentialNoCode || (hotpCredential
&& !hotpCredentialInCoolDown)
|| customPeriodCredentialNoTouch) {
if (touchCredential && !yubiKey.currentDevice.isNfc) {
navigator.snackBar(qsTr("Touch your YubiKey"))
}
if (hotpCredential && !yubiKey.currentDevice.isNfc) {
hotpTouchTimer.start()
}
yubiKey.calculate(credential, function (resp) {
if (resp.success) {
hotpTouchTimer.stop()
// This should not be needed, but it
// makes the UI update instantly.
code = resp.code
credential = resp.credential
if (copy) {
copyCode(resp.code.value)
}
if (hotpCredential) {
coolDownHotpCredential()
}
entries.updateEntry(resp)
} else {
if (resp.error_id === 'access_denied') {
if (!yubiKey.currentDevice.isNfc) {
navigator.snackBarError(qsTr("Touch timed out"))
} else {
navigator.snackBar(qsTr("Re-tap your YubiKey"))
}
} else {
navigator.snackBarError(navigator.getErrorMessage(
resp.error_id))
if (resp.error_id === 'no_device_custom_reader') {
yubiKey.clearCurrentDeviceAndEntries()
}
}
console.log("calculate failed:", resp.error_id)
}
})
} else {
copyCode(code.value)
}
}
function clearExpiredCode() {
code = null // To update UI instantly
entries.clearCode(credential.key)
}
function deleteCard() {
navigator.confirm({
"heading": qsTr("Delete %1 ?").arg(formattedName()),
"message": qsTr("This will permanently delete the account from your YubiKey."),
"description": qsTr("Before proceeding:<ul style=\"-qt-list-indent: 1;\"><li>You will not be able to generate security codes for the account anymore.<li>Make sure 2FA has been disabled on the web service.</ul>"),
"buttonAccept": qsTr("Delete account"),
"acceptedCb": function () {
yubiKey.deleteCredential(credential,
function (resp) {
if (resp.success) {
if (favorite)
{
toggleFavorite()
}
entries.deleteEntry(
credential.key)
yubiKey.updateNextCalculateAll()
navigator.snackBar(
qsTr("Account deleted"))
} else {
navigator.snackBarError(
navigator.getErrorMessage(resp.error_id))
console.log("delete failed:", resp.error_id)
if (resp.error_id === 'no_device_custom_reader') {
yubiKey.clearCurrentDeviceAndEntries()
}
}
})
}
})
}
function getCodeLblValue() {
if (!!code && !!code.value && (code.valid_to > Utils.getNow())) {
return formattedCode(code.value)
} else if (touchCredential || hotpCredential) {
return "*** ***"
} else {
return ""
}
}
function coolDownHotpCredential() {
hotpCredentialInCoolDown = true
hotpCoolDownTimer.start()
}
property string codeLabelText: getCodeLblValue()
property var isKeyChanged: false
background: Rectangle {
anchors.left: parent.left
anchors.top: parent.top
Layout.minimumWidth: 299
Layout.minimumHeight: 75
width: parent.width - 1
height: parent.height - 1
color: yubicoWhite
opacity: if (credentialCard.GridView.isCurrentItem) {
return cardSelectedEmphasis
} else if (cardMouseArea.containsMouse) {
return cardHoveredEmphasis
} else {
return isDark() ? cardNormalEmphasis : 1.0
}
MouseArea {
id: cardMouseArea
hoverEnabled: true
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onDoubleClicked: calculateCard(true)
onClicked: {
if (mouse.button === Qt.RightButton) {
contextMenu.popup()
} else {
credentialCard.GridView.isCurrentItem ? credentialCard.GridView.view.currentIndex = -1 : credentialCard.GridView.view.currentIndex = index
navigator.forceActiveFocus()
}
}
Menu {
id: contextMenu
MenuItem {
icon.source: "../images/copy.svg"
icon.color: primaryColor
opacity: highEmphasis
icon.width: 20
icon.height: 20
text: qsTr("Copy to clipboard")
onTriggered: calculateCard(true)
}
MenuItem {
icon.source: favorite ? "../images/star.svg" : "../images/star_border.svg"
icon.color: primaryColor
opacity: highEmphasis
icon.width: 20
icon.height: 20
text: favorite ? qsTr("Remove as favorite") : qsTr("Set as favorite")
onTriggered: toggleFavorite()
}
MenuSeparator {
}
MenuItem {
icon.source: "../images/delete.svg"
icon.color: primaryColor
opacity: highEmphasis
icon.width: 20
icon.height: 20
text: "Delete account"
onTriggered: deleteCard()
}
}
}
ToolTip {
text: qsTr("Double-click to initiate touch")
delay: 1000
parent: credentialCard
visible: touchCredentialNoCode && parent.hovered && !favoriteBtn.hovered
Material.foreground: toolTipForeground
Material.background: toolTipBackground
}
}
Timer {
id: hotpCoolDownTimer
triggeredOnStart: false
interval: 5000
onTriggered: hotpCredentialInCoolDown = false
}
Timer {
id: hotpTouchTimer
triggeredOnStart: false
interval: 500
onTriggered: navigator.snackBar(qsTr("Touch your YubiKey"))
}
Item {
anchors.fill: parent
CredentialCardIcon {
id: icon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 4
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
size: 40
Accessible.ignored: true
}
ColumnLayout {
anchors.left: icon.right
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
spacing: 0
Label {
id: codeLbl
font.pixelSize: 24
color: primaryColor
opacity: hovered || credentialCard.GridView.isCurrentItem ? fullEmphasis : highEmphasis
text: getCodeLblValue()
font.weight: credentialCard.GridView.isCurrentItem ? Font.Normal : Font.Light
}
Label {
id: nameLbl
text: searchQuery.length > 0 ? colorizeMatch(formattedName(), searchQuery) : formattedName()
textFormat: TextEdit.RichText
Layout.maximumWidth: credentialCard.width - 106
font.pixelSize: 14
elide: Text.ElideRight
color: primaryColor
opacity: lowEmphasis
}
ToolTip {
text: qsTr(nameLbl.text)
delay: 1000
parent: nameLbl
visible: nameLbl.truncated && credentialCard.hovered && !favoriteBtn.hovered
Material.foreground: toolTipForeground
Material.background: toolTipBackground
}
}
Accessible.role: Accessible.ListItem
Accessible.focusable: true
Accessible.name: !!credential ? (credential.issuer ? credential.issuer : credential.name) : ""
Accessible.description: getCodeLblValue()
ToolButton {
id: favoriteBtn
Layout.alignment: Qt.AlignRight | Qt.AlignTop
visible: favorite || credentialCard.hovered || credentialCard.GridView.isCurrentItem
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: -6
anchors.topMargin: -2
onClicked: toggleFavorite()
Keys.onReturnPressed: toggleFavorite()
Keys.onEnterPressed: toggleFavorite()
focusPolicy: Qt.NoFocus
Accessible.role: Accessible.Button
Accessible.name: "Favorite"
Accessible.description: "Favorite credential"
ToolTip {
text: favorite ? qsTr("Remove as favorite") : qsTr("Set as favorite")
delay: 1000
parent: favoriteBtn
visible: parent.hovered
Material.foreground: toolTipForeground
Material.background: toolTipBackground
}
icon.source: favorite ? "../images/star.svg" : "../images/star_border.svg"
icon.color: hovered || favorite ? iconFavorite : primaryColor
opacity: hovered || favorite ? highEmphasis : disabledEmphasis
implicitHeight: 30
implicitWidth: 30
MouseArea {
id: favoriteMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true
enabled: false
}
}
CredentialCardTimer {
period: credential && credential.period ? credential.period : 0
validTo: code && code.valid_to ? code.valid_to : 0
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: 3
anchors.bottomMargin: 6
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
visible: code && code.value && credential && credential.oath_type === "TOTP" ? true : false
onTimesUp: {
if (touchCredential) {
clearExpiredCode()
}
if (customPeriodCredentialNoTouch) {
calculateCard(false)
}
}
}
StyledImage {
id: touchIcon
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: 0
anchors.bottomMargin: 6
iconWidth: 18
iconHeight: 18
source: "../images/touch.svg"
visible: touchCredentialNoCode
color: primaryColor
opacity: lowEmphasis
Layout.alignment: Qt.AlignRight
}
StyledImage {
id: hotpIcon
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: -1
anchors.bottomMargin: 2
iconWidth: 20
iconHeight: 20
source: "../images/refresh.svg"
visible: hotpCredential
color: primaryColor
opacity: hotpCredentialInCoolDown ? disabledEmphasis : lowEmphasis
Layout.alignment: Qt.AlignRight
}
}
}