Files
pay/guis/desktop/main.qml
T
tomFlowee 951ae9d310 Avoid complexity; just use the checkboxes
User research showed that the bigger 'checkboxes' were not worth the
extra clicks.
As such they are removed again.
2025-06-19 17:06:39 +02:00

864 lines
35 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.Basic as QQC2
import QtQuick.Templates as T
import QtQuick.Layouts
import "../Flowee" as Flowee
import "../ControlColors.js" as ControlColors
QQC2.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)
if (!Fiat.hasPrice && Pay.isMainChain)
return "?"
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
QQC2.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
QQC2.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
Flowee.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
}
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
Flowee.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
Flowee.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
}
Flowee.Label {
text: qsTr("Unconfirmed", "balance (money)") + ":"
Layout.alignment: Qt.AlignRight
}
Flowee.BitcoinAmountLabel {
value: extraBalances.account == null ? 0 : extraBalances.account.balanceUnconfirmed
colorize: false
showFiat: false
}
Flowee.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 {} }
}
Flowee.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
Flowee.Label {
id: fiatValue
property double prevPrice: 0
text: {
if (Fiat.hasPrice)
var price = Fiat.formattedPrice(100000000, Fiat.price);
else
price = "?";
qsTr("1 BCH is: %1").arg(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
Flowee.Label {
id: netStatus
text: qsTr("Network status")
opacity: 0.6
visible: isLoading || !portfolio.current.uptodate
}
Flowee.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 {} }
}
Flowee.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
Flowee.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
}
}
}
}