7d66419c3e
This uses the 'behind' when it is far behind and updates the time since the last block when it is not.
650 lines
29 KiB
QML
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 + " / " + Pay.chainHeight + ")</p>"
|
|
let title = qsTr("Sync Status")
|
|
var time = syncLabel.time
|
|
if (time === "")
|
|
return "<p>" + title + ": " + height + " / " + targetHeight + "</p>"
|
|
return "<p>" + title + ": " + time + " ("
|
|
+ height + " / " + 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()
|
|
}
|
|
}
|
|
}
|