Files
pay/guis/desktop/main.qml
T
tomFlowee b82bf5c753 Add quick recieve on lock screen.
This moves the creation of the portfolio to happen the moment we
finished loading. (wallets were loaded either way)
The networking is the part that now waits for the user to unlock before
it does anything.
2025-02-07 00:13:59 +01:00

902 lines
36 KiB
QML

/*
* This file is part of the Flowee project
* Copyright (C) 2020-2025 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.Controls as QQC2
import QtQuick.Layouts
import "../Flowee" as Flowee
import "../ControlColors.js" as ControlColors
import QtQuick.Controls.Basic
ApplicationWindow {
id: mainWindow
visible: true
width: Pay.windowWidth
height: Pay.windowHeight
minimumWidth: 400
minimumHeight: 300
title: "Flowee Pay"
onWidthChanged: Pay.windowWidth = width
onHeightChanged: Pay.windowHeight = height
onVisibleChanged: if (visible) ControlColors.applySkin(mainWindow)
property bool isLoading: typeof net === "undefined";
Component.onCompleted: updateFontSize(mainWindow);
function updateFontSize(window) {
// 75% = > 14.25, 100% => 19, 200% => 28
window.font.pixelSize = 17 + (11 * (Pay.fontScaling-100) / 100)
}
Connections {
target: Pay
function onFontScalingChanged() {
updateFontSize(mainWindow);
if (txDetailsWindow.status === Loader.Ready)
updateFontSize(txDetailsWindow.item);
if (netView.status === Loader.Ready)
updateFontSize(netView.item);
}
function onUseDarkSkinChanged() {
ControlColors.applySkin(mainWindow);
if (txDetailsWindow.status === Loader.Ready)
ControlColors.applySkin(txDetailsWindow.item);
if (netView.status === Loader.Ready)
ControlColors.applySkin(netView.item);
}
}
onIsLoadingChanged: {
if (!isLoading) {
// 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"
if (Intent.paymentUrl !== "") {
// respond to payment intent is prio one.
tabbar.currentIndex = 1;
}
else if (!portfolio.current.isUserOwned) {
// Open on receive tab if the wallet is effectively empty
tabbar.currentIndex = 2;
}
else {
tabbar.currentIndex = 0;
}
}
}
property color floweeSalmon: "#ff9d94"
property color floweeBlue: "#0b1088"
property color floweeGreen: "#90e4b5"
property color errorRed: Pay.useDarkSkin ? "#ff6568" : "#940000"
property color errorRedBg: Pay.useDarkSkin ? "#671314" : "#9f1d1f"
Item {
id: mainScreen
anchors.fill: parent
focus: true
Loader {
id: effects
source: "./BlurComponents.qml"
}
Rectangle {
id: header
color: Pay.useDarkSkin ? "#00000000" : mainWindow.floweeBlue
width: parent.width
height: 90
Rectangle {
color: mainWindow.floweeBlue
opacity: Pay.useDarkSkin ? 1 : 0
width: 60
height: 60
radius: 30
x: 3
y: 11
Behavior on opacity { NumberAnimation { duration: 300 } }
}
Image {
id: appLogo
anchors.verticalCenter: parent.verticalCenter
x: 17
smooth: true
source: "qrc:/FloweePay-light.svg"
// ratio: 77 / 449
height: 40
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: totalBalance.width
height: totalBalance.height
anchors.bottom: parent.bottom
anchors.bottomMargin: tabbar.headerHeight + 5
anchors.right: parent.right
anchors.rightMargin: 10
baselineOffset: totalBalance.baselineOffset
Flowee.BitcoinAmountLabel {
id: totalBalance
value: {
if (isLoading)
return 0;
if (Pay.hideBalance)
return 88888888;
return portfolio.totalBalance
}
colorize: false
color: "white"
showFiat: false
fontPixelSize: 28
layer.enabled: effects.loaded && Pay.hideBalance
layer.effect: effects.item.biggerBlur
}
}
Flowee.Label {
id: totalFiatLabel
anchors.baseline: balanceInHeader.baseline
anchors.right: balanceInHeader.left
anchors.rightMargin: 10
color: "white"
font.pixelSize: 15
text: {
if (Pay.hideBalance && Pay.isMainChain)
return Fiat.formattedPrice(100000000, Fiat.price)
return Fiat.formattedPrice(totalBalance.value, Fiat.price)
}
visible: balanceInHeader.visible
opacity: 0.6
layer.enabled: effects.loaded && Pay.hideBalance
layer.effect: effects.item.simpleBlur
}
}
Item {
id: tabbedPane
width: Math.max(parent.width - 270, 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"
}
TabBarWidget {
id: tabbar
anchors.fill: parent
Pane {
id: activityTab
property string title: qsTr("Activity")
property string icon: "qrc:/activityIcon-light.png"
anchors.fill: parent
Rectangle {
anchors.fill: parent
anchors.margins: -10
color: palette.light
radius: 10
}
Column {
id: activityHeader
width: parent.width
spacing: 6
Item {
width: parent.width
height: 40
Loader {
source: isLoading ? "" : "./ActivityConfigBar.qml"
width: parent.width
y: -13
}
}
Rectangle {
width: parent.width
height: visible ? (warn.height + unarchiveButton.height + 26) : 0
color: Pay.useDarkSkin ? "#c1ba58" : "#f6e992" // yellow
visible: !isLoading && portfolio.current.isArchived
radius: 7
Flowee.Label {
id: warn
y: 6
x: 6
width: parent.width - 10
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: visible ? (decryptText.height + decryptPwd.height + decryptButton.height + 36): 0
color: Pay.useDarkSkin ? "#c1ba58" : "#f6e992" // yellow
visible: !isLoading && portfolio.current.needsPinToOpen
&& !portfolio.current.isDecrypted
radius: 7
onVisibleChanged: {
decryptError.visible = false
decryptPwd.text = ""
}
Text {
id: decryptText
y: 6
x: 6
width: parent.width - 20
horizontalAlignment: Text.AlignHCenter
color: "black"
font.pixelSize: activityTab.font.pixelSize
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
font: activityTab.font
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.pixelSize: activityTab.font.pixelSize
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
model: isLoading ? 0 : portfolio.current.transactions
clip: true
delegate: Transaction {
width: activityView.width
color: (index % 2) == 0 ? palette.light : palette.alternateBase
}
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 + 12
height: label.height + 12
radius: 5
color: palette.light
Label {
id: label
anchors.centerIn: parent
color: palette.dark
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
Item {
id: popupTabsOverlay
property alias isOpen: thePopup.visible
anchors.fill: parent
Rectangle {
color: "black"
opacity: 0.3
}
visible: isOpen
function open(sourceComponent, rightAnchor) {
thePopup.anchorTopRight = popupTabsOverlay.mapFromGlobal(rightAnchor);
loader.sourceComponent = sourceComponent;
}
QQC2.Popup {
id: thePopup
property point anchorTopRight: Qt.point(0, 0);
closePolicy: QQC2.Popup.CloseOnEscape + QQC2.Popup.CloseOnReleaseOutside
width: loader.width + 20
height: loader.height + 20
modal: true
leftPadding: 10
topPadding: 10
visible: false
onVisibleChanged: if (!visible) loader.sourceComponent = undefined;
background: Rectangle {
color: palette.light
border.color: palette.midlight
border.width: 1
radius: 6
}
Loader {
id: loader
onLoaded: thePopup.visible = true
onWidthChanged: {
height = loader.item.height
thePopup.x = thePopup.anchorTopRight.x - width
thePopup.y = thePopup.anchorTopRight.y
}
}
}
}
}
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"
}
Item {
// the whole area left of the tabbed panels.
id: overviewPane
anchors.left: parent.left
anchors.leftMargin: 6
anchors.right: tabbedPane.left
anchors.rightMargin: 12
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
anchors.top: header.bottom
Column {
id: balances
spacing: 3
width: parent.width
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
height: 14
width: 14
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
Pay.hideBalance = !Pay.hideBalance;
balanceDetailsPane.showDetails = false;
}
}
}
}
Item {
id: balanceDetailsPane
property bool showDetails: false
width: parent.width
clip: !Pay.hideBalance // on to avoid the balance overlapping the tabbar.
height: balance.height + (showDetails ? extraBalances.height + 10 : 0)
Flowee.BitcoinAmountLabel {
id: balance
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: palette.windowText
fontPixelSize: {
if (leftColumn.width < 240) // max width is 252
return leftColumn.width / 9
return 27;
}
layer.enabled: effects.loaded && Pay.hideBalance
layer.effect: effects.item.biggerBlur
}
GridLayout {
id: extraBalances
visible: parent.showDetails
width: parent.width / 0.9
anchors.top: balance.bottom
anchors.topMargin: 5
columns: 2
scale: 0.9
transformOrigin: Item.TopLeft
clip: true
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
Layout.fillWidth: true
}
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: palette.window
opacity: 0.8
anchors.fill: parent
anchors.topMargin: 2
visible: !mainWindow.isLoading && portfolio.current.needsPinToOpen && !portfolio.current.isDecrypted
}
Behavior on height { NumberAnimation {} }
}
Label {
text: {
if (mainWindow.isLoading || !Pay.isMainChain)
return "";
if (Pay.hideBalance || (portfolio.current.needsPinToOpen && !portfolio.current.isDecrypted))
return "-- " + Fiat.currencyName;
return Fiat.formattedPrice(balance.value, Fiat.price);
}
opacity: 0.6
layer.enabled: effects.loaded && Pay.hideBalance
layer.effect: effects.item.simpleBlur
}
Item { width: 1; height: fiatValue.visible ? 10 : 0 } // spacer
Item {
width: parent.width
height: fiatValue.height
Label {
id: fiatValue
property double prevPrice: 0
text: qsTr("1 BCH is: %1").arg(Fiat.formattedPrice(100000000, Fiat.price))
visible: Pay.isMainChain
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 = palette.windowText
}
}
AccountConfigMenu {
anchors.right: parent.right
visible: isLoading ? false : portfolio.singleAccountSetup
account: isLoading ? null : portfolio.current
}
}
}
Flickable {
anchors {
left: balances.left
right: balances.right
top: balances.bottom
topMargin: 8
bottom: parent.bottom
bottomMargin: 8
}
contentWidth: leftColumn.width
contentHeight: leftColumn.height
flickableDirection: Flickable.VerticalFlick
clip: true
Column {
id: leftColumn
width: balances.width
Label {
id: netStatus
text: qsTr("Network status")
opacity: 0.6
visible: isLoading || !portfolio.current.uptodate
}
Label {
visible: netStatus.visible
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: 1
height: 20
visible: netStatus.visible
}
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: 1
height: 20
}
Flowee.BigButton { // button 'add bitcoin cash wallet'
text: qsTr("Add Bitcoin Cash wallet")
onClicked: newAccountPane.source = "./NewAccountPane.qml"
isMainButton: true
width: leftColumn.width
}
Item { // spacer
width: 1
height: 20
}
Item {
// archived wallets label with hide button
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 + 6
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: 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: (event)=> {
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
item.font = mainWindow.font
}
Connections {
id: netViewHandler
function onVisibleChanged() {
if (!netView.item.visible)
netView.source = ""
}
}
}
Loader {
id: txDetailsWindow
function openTab(walletIndex) {
source = "./TransactionDetails.qml"
if (item.visible)
item.requestActivate();
item.openTab(walletIndex);
}
onLoaded: {
ControlColors.applySkin(item);
txDetailsHandler.target = item;
item.font = mainWindow.font
item.show();
}
Connections {
id: txDetailsHandler
function onVisibleChanged() {
if (!txDetailsWindow.item.visible)
txDetailsWindow.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;
}
}
}
}
// QRScanner window
QQC2.ApplicationWindow {
id: scannerWindow
visible: CameraController.visible
minimumWidth: 300
minimumHeight: 600
width: 400
height: 700
title: qsTr("QR-Scan")
flags: Qt.Dialog
onClosing: CameraController.abort();
Flowee.QRScanner {
anchors.fill: parent
}
}
}
}