Files
pay/desktop/main.qml
T

825 lines
33 KiB
QML

/*
* This file is part of the Flowee project
* Copyright (C) 2020-2022 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 2.11
import QtQuick.Controls 2.11
import QtQuick.Layouts 1.11
import QtGraphicalEffects 1.0
import "widgets" as Flowee
import "./ControlColors.js" as ControlColors
ApplicationWindow {
id: mainWindow
visible: true
width: Pay.windowWidth === -1 ? 750 : Pay.windowWidth
height: Pay.windowHeight === -1 ? 500 : Pay.windowHeight
minimumWidth: 800
minimumHeight: 600
title: "Flowee Pay"
onWidthChanged: Pay.windowWidth = width
onHeightChanged: Pay.windowHeight = height
onVisibleChanged: if (visible) ControlColors.applySkin(mainWindow)
property bool isLoading: typeof portfolio === "undefined";
onIsLoadingChanged: {
if (!isLoading) {
if (!portfolio.current.isUserOwned) { // Open on receive tab if the wallet is effectively empty
tabbar.currentIndex = 2;
}
else {
tabbar.currentIndex = 0;
}
// delay loading to avoid errors due to not having a portfolio
// portfolio is only initialized after a second or so.
receivePane.source = "./ReceiveTransactionPane.qml"
sendTransactionPane.source = "./SendTransactionPane.qml"
}
}
property color floweeSalmon: "#ff9d94"
property color floweeBlue: "#0b1088"
property color floweeGreen: "#90e4b5"
Item {
id: mainScreen
anchors.fill: parent
focus: true
Rectangle {
id: header
color: Pay.useDarkSkin ? "#00000000" : mainWindow.floweeBlue
width: parent.width
height: {
var h = mainWindow.height;
if (h > 800)
return 120;
return h / 800 * 120;
}
Rectangle {
color: mainWindow.floweeBlue
opacity: Pay.useDarkSkin ? 1 : 0
width: parent.height / 5 * 4
height: width
radius: width / 2
x: 2
y: 8
Behavior on opacity { NumberAnimation { duration: 300 } }
}
Image {
id: appLogo
anchors.verticalCenter: parent.verticalCenter
x: 20
smooth: true
source: "qrc:/FloweePay-light.svg"
// ratio: 77 / 449
height: (parent.height - 20) * 7 / 10
width: height * 449 / 77
}
Item {
id: balanceInHeader
visible: {
if (mainWindow.isLoading)
return false;
if (portfolio.accounts.length <= 1)
return false;
// If there is not enough space here (only for quite long balances), move to the left bar
var minX = appLogo.width + totalFiatLabel.width + 50 // 50 is spacing
return x > minX;
}
width: totalBalance2.width
height: totalBalance2.height
anchors.bottom: parent.bottom
anchors.bottomMargin: tabbar.headerHeight + 5
anchors.right: parent.right
anchors.rightMargin: 10
baselineOffset: totalBalance2.baselineOffset
Flowee.BitcoinAmountLabel {
id: totalBalance2
value: {
if (isLoading)
return 0;
if (Pay.hideBalance)
return 88888888;
return portfolio.totalBalance
}
colorize: false
color: "white"
showFiat: false
fontPtSize: mainWindow.font.pointSize * 2
opacity: blurredTotalBalance2.visible ? 0 : 1
}
FastBlur {
id: blurredTotalBalance2
anchors.fill: parent
anchors.margins: 5
visible: Pay.hideBalance
source: totalBalance2
radius: 58
}
}
Label {
id: totalFiatLabel
anchors.baseline: balanceInHeader.baseline
anchors.right: balanceInHeader.left
anchors.rightMargin: 10
color: "white"
text: {
if (Pay.hideBalance && Pay.isMainChain)
return "-- " + Fiat.currencyName
return Fiat.formattedPrice(totalBalance.value, Fiat.price)
}
visible: balanceInHeader.visible
opacity: 0.6
}
}
Item {
id: tabbedPane
width: parent.width * 65 / 100
anchors.right: parent.right
anchors.top: header.bottom
anchors.topMargin: -1 * tabbar.headerHeight
anchors.bottom: parent.bottom
Rectangle {
anchors.fill: parent
opacity: 0.2
anchors.topMargin: -5
anchors.leftMargin: -5
radius: 5
color: Pay.useDarkSkin ? "black" : "white"
}
Flowee.TabBar {
id: tabbar
anchors.fill: parent
Pane {
property string title: qsTr("Activity")
property string icon: "qrc:/activityIcon-light.png"
anchors.fill: parent
Rectangle {
anchors.fill: parent
anchors.margins: -10
color: mainWindow.palette.light
radius: 10
}
Column {
id: activityHeader
width: parent.width
spacing: 10
Rectangle {
width: parent.width
height: warn.height + unarchiveButton.height + 26
color: Pay.useDarkSkin ? "#c1ba58" : "#f6e992"
visible: !isLoading && portfolio.current.isArchived
radius: 10
Text {
id: warn
y: 10
x: 10
width: parent.width - 20
horizontalAlignment: Text.AlignHCenter
color: "black"
font.bold: true
wrapMode: Text.WordWrap
text: qsTr("Archived wallets do not check for activities. Balance may be out of date.")
}
Flowee.Button {
id: unarchiveButton
text: qsTr("Unarchive")
anchors.right: warn.right
anchors.top: warn.bottom
anchors.topMargin: 6
onClicked: portfolio.current.isArchived = false
}
}
Rectangle {
id: needsDecryptPane
width: parent.width
height: decryptText.height + decryptPwd.height + decryptButton.height + 36
color: Pay.useDarkSkin ? "#c1ba58" : "#f6e992"
visible: !isLoading && portfolio.current.needsPinToOpen
&& !portfolio.current.isDecrypted
radius: 10
onVisibleChanged: {
decryptError.visible = false
decryptPwd.text = ""
}
Text {
id: decryptText
y: 10
x: 10
width: parent.width - 20
horizontalAlignment: Text.AlignHCenter
color: "black"
font.bold: true
wrapMode: Text.WordWrap
text: qsTr("This wallet needs a password to open.")
}
Text {
id: decryptLabel
anchors.left: decryptText.left
anchors.verticalCenter: decryptPwd.verticalCenter
color: decryptText.color
text: qsTr("Password:")
}
Flowee.TextField {
id: decryptPwd
focus: needsDecryptPane.visible
anchors.top: decryptText.bottom
anchors.left: decryptLabel.right
anchors.right: parent.right
anchors.margins: 6
echoMode: TextInput.Password
onAccepted: decryptButton.clicked()
}
Text {
id: decryptError
anchors.left: decryptPwd.left
anchors.verticalCenter: decryptButton.verticalCenter
color: "#830000"
font.bold: true
text: qsTr("Invalid password")
visible: false
}
Flowee.Button {
id: decryptButton
text: qsTr("Open")
anchors.right: decryptText.right
anchors.top: decryptPwd.bottom
anchors.topMargin: 6
enabled: decryptPwd.text !== ""
onClicked: {
portfolio.current.decrypt(decryptPwd.text)
if (portfolio.current.isDecrypted) {
decryptPwd.text = ""
decryptError.visible = false
} else {
decryptPwd.selectAll();
decryptError.visible = true
decryptPwd.forceActiveFocus();
}
}
}
}
}
ListView {
id: activityView
/*
Warning; (Qt5.15) do NOT guard the below `model` line with any isLoading stuff, it will
break showing the model properly after the default wallet gets decrypted just
after start.
*/
model: portfolio.current.transactions
clip: true
delegate: WalletTransaction { width: activityView.width }
anchors.top: activityHeader.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
ScrollBar.vertical: Flowee.ScrollThumb {
id: thumb
minimumSize: 20 / activityView.height
visible: size < 0.9
preview: Rectangle {
width: label.width + 20
height: label.height + 20
radius: 5
color: label.palette.dark
Label {
id: label
anchors.centerIn: parent
color: palette.light
text: isLoading || activityView.model === null ? "" : activityView.model.dateForItem(thumb.position);
}
}
}
onModelChanged: resetUnreadTimer.restart();
Timer {
id: resetUnreadTimer
interval: 90 * 1000
// remove the 'new transaction' indicator.
onTriggered: portfolio.current.transactions.lastSyncIndicator = undefined
running: activityView.enabled && activityView.visible
// copy the height so we know when the account height changes
// and we can restart our timer as a result
property int height: {
if (isLoading) return 0;
return portfolio.current.lastBlockSynched
}
onHeightChanged: restart();
}
}
Keys.forwardTo: Flowee.ListViewKeyHandler {
target: activityView
}
}
Loader {
id: sendTransactionPane
// Disable these tabs for archived accounts
enabled: !mainWindow.isLoading && !portfolio.current.isArchived
&& (!portfolio.current.needsPinToOpen || portfolio.current.isDecrypted)
anchors.fill: parent
property string title: qsTr("Send")
property string icon: "qrc:/sendIcon-light.png"
}
Loader {
id: receivePane
enabled: sendTransactionPane.enabled && (!portfolio.current.needSpinToOpen || portfolio.current.isDecrypted)
anchors.fill: parent
property string icon: "qrc:/receiveIcon.png"
property string title: qsTr("Receive")
}
SettingsPane {
anchors.fill: parent
}
}
Behavior on opacity { NumberAnimation { } }
visible: opacity > 0
}
Loader {
// This overlays the tabbed pane
id: accountOverlay
anchors.bottom: parent.bottom
anchors.left: overviewPane.right
anchors.right: parent.right
anchors.top: header.bottom
anchors.topMargin: -1 * tabbar.headerHeight
opacity: 0
Behavior on opacity { NumberAnimation { } }
states: [
State {
name: "showTransactions"
PropertyChanges { target: tabbedPane; opacity: 1 }
PropertyChanges { target: tabbar; focus: true }
PropertyChanges { target: accountOverlay;
opacity: 0;
source: "";
}
},
State {
name: "accountDetails"
PropertyChanges { target: accountOverlay;
source: "./AccountDetails.qml"
opacity: 1
focus: true
}
PropertyChanges { target: tabbedPane; opacity: 0 }
},
State {
name: "startWalletEncryption"
PropertyChanges { target: accountOverlay;
source: "./WalletEncryption.qml"
opacity: 1
focus: true
}
PropertyChanges { target: tabbedPane; opacity: 0 }
}
]
state: "showTransactions"
}
Flickable {
// the whole area left of the tabbed panels.
id: overviewPane
anchors.left: parent.left
anchors.right: tabbedPane.left
anchors.bottom: parent.bottom
anchors.bottomMargin: 10
anchors.top: header.bottom
contentWidth: leftColumn.width
contentHeight: leftColumn.height
flickableDirection: Flickable.VerticalFlick
clip: true
Column {
id: leftColumn
x: 10
width: overviewPane.width - 60
// the total balance is for most people not this one, but the balanceInHeader one.
// we only show this one when the header one decides it can't be seen.
Label {
id: totalBalanceLabel
visible: (mainWindow.isLoading || portfolio.accounts.length > 1) && !balanceInHeader.visible
text: qsTr("Total balance");
height: implicitHeight / 10 * 9
}
Item {
visible: totalBalanceLabel.visible
width: totalBalance.width
height: totalBalance.height
Flowee.BitcoinAmountLabel {
id: totalBalance
value: {
if (isLoading)
return 0;
if (Pay.hideBalance)
return 88888888;
return portfolio.totalBalance
}
colorize: false
showFiat: false
fontPtSize: mainWindow.font.pointSize * 2
opacity: blurredTotalBalance.visible ? 0 : 1
}
FastBlur {
id: blurredTotalBalance
anchors.fill: parent
anchors.margins: -5
visible: Pay.hideBalance
source: totalBalance
radius: 58
}
}
Label {
text: {
if (Pay.hideBalance && Pay.isMainChain)
return "-- " + Fiat.currencyName
return Fiat.formattedPrice(totalBalance.value, Fiat.price)
}
visible: totalBalanceLabel.visible
opacity: 0.6
}
Item { // spacer
width: 10
height: 50
}
Item {
height: balanceLabel.height
width: parent.width
Label {
id: balanceLabel
text: qsTr("Balance");
height: implicitHeight / 10 * 7
}
Image {
id: showBalanceButton
anchors.right: parent.right
source: {
var state = Pay.hideBalance ? "closed" : "open";
var skin = Pay.useDarkSkin ? "-light" : ""
return "qrc:/eye-" + state + skin + ".png";
}
smooth: true
opacity: 0.5
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Pay.hideBalance = !Pay.hideBalance;
balanceDetailsPane.showDetails = false;
}
}
}
Label {
id: questionMark
text: "🛈"
ToolTip.text: qsTr("Show Wallet Details")
anchors.right: showBalanceButton.left
anchors.baseline: balanceLabel.baseline
anchors.rightMargin: 10
opacity: 0.5
visible: !isLoading && !portfolio.current.isUserOwned
MouseArea {
anchors.fill: parent
onClicked: accountOverlay.state = "accountDetails";
}
}
}
Item {
id: balanceDetailsPane
property bool showDetails: false
width: balance.width
height: balance.height + (showDetails ? extraBalances.height + 20 : 0)
Flowee.BitcoinAmountLabel {
id: balance
opacity: blurredBalance.visible ? 0 : 1
value: {
if (isLoading)
return 0;
var account = portfolio.current;
if (account === null)
return 0;
if (Pay.hideBalance)
return 88888888;
return account.balanceConfirmed + account.balanceUnconfirmed
}
colorize: false
showFiat: false
color: mainWindow.palette.text
fontPtSize: {
if (leftColumn.width < 300)
return mainWindow.font.pointSize * 2
return mainWindow.font.pointSize * 3
}
}
FastBlur {
id: blurredBalance
anchors.fill: parent
anchors.margins: -5
visible: Pay.hideBalance
source: balance
radius: 58
}
GridLayout {
id: extraBalances
visible: parent.showDetails
width: parent.width - 30
anchors.top: balance.bottom
anchors.topMargin: 10
columns: 2
x: 25
property QtObject account: mainWindow.isLoading ? null : portfolio.current
Label {
text: qsTr("Main", "balance (money), non specified") + ":"
Layout.alignment: Qt.AlignRight
}
Flowee.BitcoinAmountLabel {
value: extraBalances.account == null ? 0 : extraBalances.account.balanceConfirmed
colorize: false
showFiat: false
}
Label {
text: qsTr("Unconfirmed", "balance (money)") + ":"
Layout.alignment: Qt.AlignRight
}
Flowee.BitcoinAmountLabel {
value: extraBalances.account == null ? 0 : extraBalances.account.balanceUnconfirmed
colorize: false
showFiat: false
}
Label {
text: qsTr("Immature", "balance (money)") + ":"
Layout.alignment: Qt.AlignRight
}
Flowee.BitcoinAmountLabel {
value: extraBalances.account == null ? 0 : extraBalances.account.balanceImmature
colorize: false
showFiat: false
}
}
MouseArea {
enabled: priceCover.visible === false
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!Pay.hideBalance)
parent.showDetails = !parent.showDetails
}
}
Rectangle {
id: priceCover // this covers the prices while the wallet is encrypted.
color: mainWindow.palette.window
opacity: 0.8
anchors.fill: parent
anchors.topMargin: 6
visible: !mainWindow.isLoading && portfolio.current.needsPinToOpen && !portfolio.current.isDecrypted
}
Behavior on height { NumberAnimation {} }
}
Label {
text: {
if (mainWindow.isLoading)
return "";
if (Pay.hideBalance && Pay.isMainChain
|| (portfolio.current.needsPinToOpen && !portfolio.current.isDecrypted))
return "-- " + Fiat.currencyName;
return Fiat.formattedPrice(balance.value, Fiat.price);
}
opacity: 0.6
}
Item { // spacer
visible: fiatValue.visible
width: 10
height: 20
}
Label {
id: fiatValue
property double prevPrice: 0
text: qsTr("1 BCH is: %1").arg(Fiat.formattedPrice(100000000, Fiat.price))
visible: Pay.isMainChain
font.pixelSize: 18
Behavior on color { ColorAnimation { duration: 300 } }
onTextChanged: {
animTimer.start()
if (prevPrice > Fiat.price)
color = Pay.useDarkSkin ? "#5f1414" : "#ff3636"; // red
else
color = Pay.useDarkSkin ? "#154822" : "#4aff77"; // green
prevPrice = Fiat.price
}
Timer {
id: animTimer
interval: 305
onTriggered: fiatValue.color = fiatValue.palette.text
}
}
Item { // spacer
width: 10
height: totalBalanceLabel.visible ? 40 : 10
}
Label {
text: qsTr("Network status")
opacity: 0.6
}
Label {
id: syncIndicator
text: {
if (isLoading)
return "";
var account = portfolio.current;
if (account === null)
return "";
if (account.needsPinToOpen && !account.isDecrypted)
return qsTr("Offline");
return account.timeBehind;
}
font.italic: true
}
Item { // spacer
width: 10
height: 60
}
Repeater { // the portfolio listing our accounts
width: parent.width
model: mainWindow.isLoading ? 0 : portfolio.accounts;
delegate: AccountListItem {
width: leftColumn.width
account: modelData
}
}
Item { // spacer
width: 10
height: 40
}
Rectangle { // button 'add bitcoin cash wallet'
color: mainWindow.floweeGreen
radius: 10
width: leftColumn.width
height: buttonLabel.height + 30
Text {
id: buttonLabel
anchors.centerIn: parent
width: parent.width - 20
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
text: qsTr("Add Bitcoin Cash wallet")
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: newAccountPane.source = "./NewAccountPane.qml"
}
}
Item { // spacer
width: 10
height: 40
}
Item {
visible: !isLoading && portfolio.archivedAccounts.length > 0
height: archivedLabel.height
width: leftColumn.width
Flowee.ArrowPoint {
id: showArchivedWalletsList
property bool on: false
color: Pay.useDarkSkin ? "white" : "black"
rotation: on ? 90 : 0
transformOrigin: Item.Center
Behavior on rotation { NumberAnimation {} }
}
Label {
id: archivedLabel
x: showArchivedWalletsList.width + 10
text: {
if (isLoading)
return ""
var walletCount = portfolio.archivedAccounts.length
return qsTr("Archived wallets [%1]", "Arg is wallet count", walletCount).arg(walletCount);
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: showArchivedWalletsList.on = !showArchivedWalletsList.on
}
}
Repeater { // the archived accounts
width: parent.width
model: showArchivedWalletsList.on ? portfolio.archivedAccounts : 0;
delegate: AccountListItem {
width: leftColumn.width
account: modelData
// archived accounts don't have access to anything but the activity tab
onClicked: tabbar.currentIndex = 0; // change to the 'activity' tab
}
}
}
}
Rectangle {
id: splashScreen
color: mainWindow.palette.window
anchors.fill: parent
Label {
text: qsTr("Preparing...")
anchors.centerIn: parent
font.pointSize: 20
}
opacity: mainWindow.isLoading ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 300 } }
}
Keys.onPressed: {
if ((event.modifiers & Qt.ControlModifier) !== 0) {
if (event.key === Qt.Key_Q) {
mainWindow.close()
}
}
}
// NetView (reachable from settings)
Loader {
id: netView
onLoaded: {
ControlColors.applySkin(item)
netViewHandler.target = item
}
Connections {
id: netViewHandler
function onVisibleChanged() {
if (!netView.item.visible)
netView.source = ""
}
}
}
// new accounts pane, corresponding to the big green button
Loader {
id: newAccountPane
anchors.fill: parent
onLoaded: {
tabbar.enabled = false // avoid it taking focus on tab
newAccountHandler.target = item // to unload on hide
}
Connections {
id: newAccountHandler
function onVisibleChanged() {
if (!newAccountPane.item.visible) {
newAccountPane.source = ""
tabbar.enabled = true // reenable
tabbar.focus = true;
}
}
}
}
}
}