Files
pay/guis/mobile/AccountsList.qml
tomFlowee 7d66419c3e Display sync status nicer.
This uses the 'behind' when it is far behind and updates the time since
the last block when it is not.
2026-05-05 19:24:42 +02:00

650 lines
29 KiB
QML

/*
* This file is part of the Flowee project
* Copyright (C) 2022-2026 Tom Zander <tom@flowee.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls.Basic as QQC2
import "../Flowee" as Flowee
import Flowee.org.pay
Page {
id: root
headerText: singleAccountSetup ? qsTr("Wallet") : qsTr("Wallets")
property QtObject newAccountAction: QQC2.Action {
text: qsTr("Add Wallet")
onTriggered: thePile.push("./NewAccount.qml")
}
menuItems: [ newAccountAction ]
function indexOfCurrentAccount() {
var list = tabBar.model
var cur = portfolio.current
for (let i = 0; i < list.length; i = i + 1) {
if (list[i] === cur) return i
}
return 0
}
// this is a special interpretation of the property-name in the context
// of these pages where (unlike in the rest of the app) we take archived
// and private wallets into account.
property bool singleAccountSetup: portfolio.rawAccounts.length === 1
property QtObject backupModule: ModuleManager.moduleInfo("backupSyncModule")
Flickable {
id: scroller
contentHeight: options.height
anchors.fill: parent
clip: true
Column {
id: options
spacing: 6
width: parent.width
SelectDefaultConfigButton {
visible: !singleAccountSetup
}
Item {
width: parent.width
height: privateButton.height
visible: !singleAccountSetup
Image {
source: "qrc:/private" + (Pay.useDarkSkin ? "-light.svg" : ".svg")
width: 20
height: 20
smooth: true
opacity: enabled ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
}
TextButton {
id: privateButton
width: parent.width - 26
x: 26
text: Pay.privateMode ? qsTr("Exit Private Mode") : qsTr("Enter Private Mode")
subtext: Pay.privateMode ? qsTr("Reveals wallets marked private")
: qsTr("Hides wallets marked private")
onClicked: {
Pay.privateMode = !Pay.privateMode
thePile.pop()
}
}
}
Item {
id: placeholder // the list of wallets will be placed on top of this.
width: parent.width
height: walletList.height
}
property QtObject account: portfolio.current
PageTitledBox {
width: parent.width
title: qsTr("Wallet Name")
EditableLabel {
text: options.account.name
onEdited: options.account.name = text
width: parent.width
}
}
PageTitledBox {
title: qsTr("Information")
width: parent.width
Flowee.AccountTypeLabel {
width: parent.width
account: options.account
font.pixelSize: root.font.pixelSize * 0.9
color: palette.brightText
}
Flowee.Label {
id: syncLabel
width: parent.width
color: palette.brightText
font.pixelSize: root.font.pixelSize * 0.9
property string time: ""
text: {
if (options.account.isArchived)
return qsTr("Offline") + " (" + options.account.timeBehind + ")"
var height = options.account.lastBlockSynched
if (height < 1)
return ""
let targetHeight = Pay.chainHeight
if (targetHeight - height > 30)
return "<p>" + options.account.timeBehind + " ("
+ height + "&nbsp;/&nbsp;" + Pay.chainHeight + ")</p>"
let title = qsTr("Sync Status")
var time = syncLabel.time
if (time === "")
return "<p>" + title + ": " + height + "&nbsp;/&nbsp;" + targetHeight + "</p>"
return "<p>" + title + ": " + time + " ("
+ height + "&nbsp;/&nbsp;" + Pay.chainHeight + ")</p>"
}
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Timer {
// the lastBlockSynchedTime does not change,
// but since we render it as '12 minutes ago'
// we need to actually re-interpret that
// ever so often to keep the relative time.
running: !options.account.isArchived
interval: 30000 // 30 sec
repeat: true
triggeredOnStart: true
onTriggered: syncLabel.time = Pay.formatDateTime(options.account.lastBlockSynchedTime)
}
}
Flowee.CheckBox {
Layout.fillWidth: true
visible: !singleAccountSetup
enabled: !options.account.isArchived
checked: options.account.isPrivate
text: qsTr("Hide in private mode")
onClicked: options.account.isPrivate = checked
}
TextButton {
text: qsTr("Addresses and keys")
subtext: qsTr("Find private keys and addresses")
enabled: !options.account.needsPinToOpen || options.account.isDecrypted
pageButton: true
onClicked: thePile.push(backupDetails)
}
TextButton {
text: qsTr("xpub")
pageButton: true
subtext: enabled ? qsTr("To connect this wallet") : qsTr("Unavailable for this wallet")
enabled: options.account.isHDWallet
&& (!options.account.needsPinToOpen || options.account.isDecrypted)
onClicked: thePile.push(xpubComponent)
Component {
id: xpubComponent
Page {
id: detailsPage
headerText: qsTr("Your XPub details")
ColumnLayout {
width: parent.width
spacing: 20
Flowee.Label {
id: titleText
text: qsTr("Be careful who you share the xpub with!")
Layout.fillWidth: true
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
Flowee.QRWidget {
id: seedQr
qrSize: 250
textVisible: false
useRawString: true
qrText: options.account.xpub
Layout.alignment: Qt.AlignHCenter
}
Flowee.Label {
text: options.account.xpub
Layout.fillWidth: true
wrapMode: Text.WrapAnywhere
font.pixelSize: titleText.font.pixelSize * 0.9
horizontalAlignment: Qt.AlignHCenter
}
}
}
}
}
}
PageTitledBox {
title: qsTr("Configuration")
width: parent.width
InstaPayConfigButton {
enabled: !options.account.isArchived
account: options.account
}
Item {
width: parent.width
height: 50
Image {
source: "qrc:/cloud-storage" + (Pay.useDarkSkin ? "-light.svg" : ".svg")
width: 35
height: 25
smooth: true
x: 5
y: 16
}
TextButton {
x: 50
width: parent.width - 50
visible: backupModule != null
enabled: options.account.isDecrypted && options.account.isHDWallet
text: qsTr("Save comments to cloud")
subtext: enabled ? qsTr("Improves wallet imports") : qsTr("Unavailable for this wallet")
pageButton: true
onClicked: thePile.push(backupModule.sectionUrl("walletOptions"),
{ "pageData": options.account })
currentValue: options.account.cloudStorageEnabled
}
}
}
PageTitledBox {
title: qsTr("Backup")
width: parent.width
Item {
id: nftOptionPanel
visible: backupModule.nfcAvailable
enabled: options.account.isDecrypted && options.account.isHDWallet
width: parent.width
height: 50
Image {
id: theLogo
source: "qrc:/backup-sync/nfc-tag.svg"
smooth: true
width: 50
height: 50
}
TextButton {
x: 56
width: parent.width - x
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Make NFC backup")
subtext: qsTr("Create a physical backup to a tag")
pageButton: true
onClicked: thePile.push(backupModule.sectionUrl("storeOnNfc"), { "pageData": options.account })
buttonId: 92387 // same as the next, as one backup is enough.
}
}
TextButton {
width: parent.width
enabled: options.account.isDecrypted
text: qsTr("Show seed for paper backup")
subtext: qsTr("The details needed to restore this wallet")
pageButton: true
onClicked: thePile.push(options.account.isHDWallet ? hdBackupDetails : backupDetails)
buttonId: 92387
Component {
id: hdBackupDetails
Page {
id: detailsPage
headerText: qsTr("Backup Details")
Item {
// non-layoutable items.
Flowee.Popup {
id: qrPopup
width: 270
height: 270
x: (root.width - width) / 2
y: 100
modal: true
closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutsideParent
background: Rectangle {
color: palette.light
border.color: palette.midlight
border.width: 1
radius: 5
}
Flowee.QRWidget {
id: seedQr
qrSize: 250
textVisible: false
useRawString: true
anchors.centerIn: parent
}
}
}
ColumnLayout {
width: parent.width
spacing: 10
PageTitledBox {
title: qsTr("Wallet seed-phrase")
Item {
implicitHeight: mnemonicLabel.implicitHeight
width: parent.width
Flowee.LabelWithClipboard {
id: mnemonicLabel
text: options.account.mnemonic
width: parent.width - 36
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
font.wordSpacing: 1
}
Image {
width: 20
height: 20
anchors.right: parent.right
source: "qrc:/qr-code" + (Pay.useDarkSkin ? "-light.svg" : ".svg")
MouseArea {
anchors.fill: parent
onClicked: {
seedQr.qrText = options.account.mnemonic
qrPopup.open()
}
}
}
}
Flowee.Label {
text: qsTr("Password")
font.bold: true
visible: options.account.mnemonicPwd !== ""
}
Flowee.LabelWithClipboard {
text: options.account.mnemonicPwd
visible: text !== ""
}
}
PageTitledBox {
title: qsTr("Seed format")
visible: options.account.isElectrumMnemonic
Flowee.Label {
text: "Electrum"
font.italic: true
}
}
PageTitledBox {
title: qsTr("Start Date", "date of wallet creation")
Flowee.LabelWithClipboard {
text: {
var startDate = options.account.firstMinedTransaction
if (!isNaN(startDate.getTime())) // date is valid
return startDate.toLocaleDateString()
return Pay.timeOfBlockHeight(options.account.accountStartBlockHeight).toLocaleDateString()
}
}
}
PageTitledBox {
title: qsTr("Derivation Path")
Flowee.LabelWithClipboard {
text: options.account.hdDerivationPath
}
}
Flowee.Label {
Layout.fillWidth: true
text: qsTr("Please save the seed-phrase on paper, in the right order, with the derivation path. This seed will allow you to recover your wallet in case you lose your mobile.")
textFormat: Text.StyledText
font.italic: true
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
Flowee.Label {
Layout.fillWidth: true
text: qsTr("<b>Important</b>: Never share your seed-phrase with others!")
font.italic: true
textFormat: Text.StyledText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
}
}
}
Component {
id: backupDetails
Page {
headerText: qsTr("Wallet keys")
property QtObject showIndexAction: QQC2.Action {
text: qsTr("Show Index", "toggle to show numbers")
checkable: true
checked: listView.showHdIndex
onTriggered: listView.showHdIndex = checked
}
menuItems: {
if (options.account.isHDWallet)
return [showIndexAction]
return []
}
PageTitledBox {
width: parent.width
id: optionsBox
Flowee.CheckBox {
text: qsTr("Change Addresses")
visible: options.account.isHDWallet
onClicked: options.account.secrets.showChangeChain = checked
toolTipText: qsTr("Switches between addresses others can pay you on, and addresses the wallet uses to send change back to yourself.")
}
Flowee.CheckBox {
text: qsTr("Used Addresses")
visible: !options.account.isSingleAddressAccount
onClicked: options.account.secrets.showUsedAddresses = checked
toolTipText: qsTr("Switches between unused and used Bitcoin addresses")
}
}
PageTitledBox {
// since the non-HD wallet type has only the 'wallet-keys' as a backup page,
// for such wallets we additionally add the starting date here.
id: startLabel
width: parent.width
visible: !options.account.isHDWallet
height: visible ? implicitHeight : 0
anchors.top: optionsBox.bottom
title: qsTr("Start Date", "date of wallet creation")
Flowee.LabelWithClipboard {
text: {
var startDate = options.account.firstMinedTransaction
if (!isNaN(startDate.getTime())) // date is valid
return startDate.toLocaleDateString()
return Pay.timeOfBlockHeight(options.account.accountStartBlockHeight).toLocaleDateString()
}
}
}
Item {
// this is a horrible hack...
// First, ListViews almost always require clipping on,
// otherwise list-items can overlap the rest of your view.
// But if I enable clipping I no longer get the nice
// width-filling backgrounds...
// Sooo. I need a clipping item that is full width (negative
// left and right margin)
id: clipItem
anchors {
top: startLabel.bottom
topMargin: 10
bottom: parent.bottom
left: parent.left
right: parent.right
leftMargin: -10
rightMargin: -10
}
clip: true
Flowee.WalletSecretsView {
id: listView
anchors {
fill: parent
leftMargin: 10
rightMargin: 10
}
account: options.account
showHdIndex: false
}
}
}
}
}
}
Item { width: 1; height: 10 } // spacer. Make the button not to close to the clickable checkbox for fatfingered people.
Rectangle {
id: archiveButton
height: archiveButtonText.height + 20
color: Pay.useDarkSkin ? "#b39554" : "#e5be6b"
width: archiveButtonText.width + 50
visible: !singleAccountSetup
radius: 3
Image {
source: "qrc:/archived.svg"
x: 6
anchors.verticalCenter: parent.verticalCenter
width: 32
height: 32
smooth: true
}
Flowee.Label {
id: archiveButtonText
text: options.account.isArchived ? qsTr("Unarchive Wallet") : qsTr("Archive Wallet")
x: 35
anchors.verticalCenter: parent.verticalCenter
color: "black"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.ArrowCursor
onClicked: options.account.isArchived = !options.account.isArchived
}
}
Item { width: 1; height: 30 } // spacer.
Rectangle {
id: removeWallet
height: archiveButtonText.height + 20
color: mainWindow.errorRedBg
width: removeWalletLabel.width + 30
visible: options.account.isArchived
radius: 3
QQC2.Label {
id: removeWalletLabel
text: qsTr("Remove Wallet")
anchors.centerIn: parent
color: "#fcfcfc"
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.ArrowCursor
onClicked: {
errorDialog.visible = true
errorDialog.forceActiveFocus()
}
}
}
Item { width: 1; height: 20 } // spacer.
}
}
MouseArea {
width: parent.width + 20
height: Math.min(180, walletList.contentHeight)
x: -10
y: Math.max(0, placeholder.y - scroller.contentY)
Flickable {
id: walletList
width: parent.width
height: parent.height
contentHeight: column.height
contentWidth: width
clip: true
boundsBehavior: height <= contentHeight ? Flickable.StopAtBounds : Flickable.DragAndOvershootBounds
Column {
id: column
x: 10
Repeater {
model: portfolio.rawAccounts
Rectangle {
width: root.width - 20
height: ali.height + 6
color: palette.light
Rectangle {
anchors.fill: parent
anchors.bottomMargin: 6
anchors.leftMargin: -3
anchors.rightMargin: -3
color: ((index % 2) === 0) ? palette.base : palette.alternateBase
radius: 6
border.width: !singleAccountSetup && options.account === modelData ? 1.3 : 0
border.color: palette.highlight
}
Column {
width: 20
anchors.verticalCenter: ali.verticalCenter
Flowee.Label {
anchors.horizontalCenter: parent.horizontalCenter
visible: modelData.isPrimaryAccount
text: "✷"
}
Image {
id: archivedLabel
source: "qrc:/archived.svg"
width: 20
height: 20
visible: modelData.isArchived
anchors.horizontalCenter: parent.horizontalCenter
smooth: true
}
Image {
source: "qrc:/private" + (Pay.useDarkSkin ? "-light.svg" : ".svg")
width: 16
height: 16
smooth: true
anchors.horizontalCenter: parent.horizontalCenter
visible: !archivedLabel.visible && modelData.isPrivate
}
Image {
source: "qrc:/cloud-storage" + (Pay.useDarkSkin ? "-light.svg" : ".svg")
width: 16
height: 13
visible: !archivedLabel.visible && modelData.cloudStorageEnabled
smooth: true
anchors.horizontalCenter: parent.horizontalCenter
}
}
AccountListItem {
id: ali
x: 22
height: implicitHeight
width: parent.width - 24
account: modelData
}
MouseArea {
anchors.fill: parent
onClicked: options.account = modelData
}
}
}
}
}
}
Item {
Flowee.Dialog {
id: errorDialog
standardButtons: QQC2.DialogButtonBox.Ok + QQC2.DialogButtonBox.Cancel
title: qsTr("Really Delete?")
text: qsTr("Removing wallet \"%1\" can not be undone.", "argument is the wallet name").arg(root.account.name)
onAccepted: options.account.removeAccount()
}
}
}