status-desktop/ui/app/AppLayouts/Wallet/panels/SwapInputPanel.qml

278 lines
11 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Shapes 1.15
import StatusQ 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Core.Theme 0.1
import shared.popups.send.views 1.0
import shared.popups.send.panels 1.0
import utils 1.0
import shared.stores 1.0
import SortFilterProxyModel 0.2
Control {
id: root
// input API
required property CurrenciesStore currencyStore
required property var flatNetworksModel
required property var processedAssetsModel
property string tokenKey
onTokenKeyChanged: {
if (!!tokenKey)
Qt.callLater(d.setSelectedHoldingId, tokenKey, Constants.TokenType.ERC20)
}
property string tokenAmount
onTokenAmountChanged: {
if (!!tokenAmount)
Qt.callLater(() => amountToSendInput.input.text = Number(tokenAmount).toLocaleString(Qt.locale(), 'f', -128))
}
property int swapSide: SwapInputPanel.SwapSide.Pay
property bool fiatInputInteractive
property bool loading
// output API
readonly property string selectedHoldingId: d.selectedHoldingId
readonly property double cryptoValue: amountToSendInput.cryptoValueToSendFloat
readonly property string cryptoValueRaw: amountToSendInput.cryptoValueToSend
readonly property bool cryptoValueValid: amountToSendInput.inputNumberValid
// visual properties
property int swapExchangeButtonWidth: 44
property string caption: swapSide === SwapInputPanel.SwapSide.Pay ? qsTr("Pay") : qsTr("Receive")
enum SwapSide {
Pay = 0,
Receive = 1
}
padding: Style.current.padding
// by design
implicitWidth: 492
implicitHeight: 131
Component.onCompleted: {
if (root.swapSide === SwapInputPanel.SwapSide.Pay)
amountToSendInput.input.forceActiveFocus()
}
QtObject {
id: d
function setSelectedHoldingId(holdingId, holdingType) {
let holding = SQUtils.ModelUtils.getByKey(root.processedAssetsModel, "symbol", holdingId)
d.selectedHoldingId = holdingId
d.setSelectedHolding(holding, holdingType)
}
function setSelectedHolding(holding, holdingType) {
d.selectedHoldingType = holdingType
d.selectedHolding = holding
holdingSelector.setSelectedItem(holding, holdingType)
}
property var selectedHolding: null
property var selectedHoldingType: Constants.TokenType.Unknown
property string selectedHoldingId
readonly property bool isSelectedHoldingValidAsset: !!selectedHolding && selectedHoldingType === Constants.TokenType.ERC20
readonly property double maxFiatBalance: isSelectedHoldingValidAsset ? selectedHolding.currentCurrencyBalance : 0
readonly property double maxCryptoBalance: isSelectedHoldingValidAsset ? selectedHolding.currentBalance : 0
readonly property double maxInputBalance: amountToSendInput.inputIsFiat ? maxFiatBalance : maxCryptoBalance
readonly property string inputSymbol: amountToSendInput.inputIsFiat ? root.currencyStore.currentCurrency :
!!d.selectedHolding && !!d.selectedHolding.symbol ? d.selectedHolding.symbol: ""
readonly property string maxInputBalanceFormatted:
root.currencyStore.formatCurrencyAmount(Math.trunc(prepareForMaxSend(d.maxInputBalance, d.inputSymbol)*100)/100, d.inputSymbol, {noSymbol: !amountToSendInput.inputIsFiat})
function prepareForMaxSend(value, symbol) {
if (symbol !== Constants.ethToken) {
return value
}
return value - Math.max(0.0001, Math.min(0.01, value * 0.1))
}
property string searchText
}
background: Shape {
id: shape
property int radius: 16
property int leftTopRadius: radius
property int rightTopRadius: radius
property int leftBottomRadius: radius
property int rightBottomRadius: radius
readonly property int cutoutGap: 4
scale: swapSide === SwapInputPanel.SwapSide.Pay ? -1 : 1
ShapePath {
id: path
fillColor: Theme.palette.indirectColor3
strokeColor: amountToSendInput.input.input.edit.activeFocus ? Theme.palette.directColor7 : Theme.palette.directColor8
strokeWidth: 1
capStyle: ShapePath.RoundCap
startX: shape.leftTopRadius
startY: 0
PathLine {
x: shape.width/2 - root.swapExchangeButtonWidth/2 - (shape.cutoutGap/2 + path.strokeWidth)
y: 0
}
PathArc { // the cutout
relativeX: root.swapExchangeButtonWidth + (shape.cutoutGap + path.strokeWidth*2)
direction: PathArc.Counterclockwise
radiusX: root.swapExchangeButtonWidth/2 + path.strokeWidth
radiusY: root.swapExchangeButtonWidth/2 - path.strokeWidth/2
}
PathLine {
x: shape.width - shape.rightTopRadius
y: 0
}
PathArc {
x: shape.width
y: shape.rightTopRadius
radiusX: shape.rightTopRadius
radiusY: shape.rightTopRadius
}
PathLine {
x: shape.width
y: shape.height - shape.rightBottomRadius
}
PathArc {
x: shape.width - shape.rightBottomRadius
y: shape.height
radiusX: shape.rightBottomRadius
radiusY: shape.rightBottomRadius
}
PathLine {
x: shape.leftBottomRadius
y: shape.height
}
PathArc {
x: 0
y: shape.height - shape.leftBottomRadius
radiusX: shape.leftBottomRadius
radiusY: shape.leftBottomRadius
}
PathLine {
x: 0
y: shape.leftTopRadius
}
PathArc {
x: shape.leftTopRadius
y: 0
radiusX: shape.leftTopRadius
radiusY: shape.leftTopRadius
}
}
}
contentItem: RowLayout {
spacing: 20
ColumnLayout {
Layout.preferredWidth: parent.width*.66
Layout.fillHeight: true
AmountToSend {
Layout.fillWidth: true
id: amountToSendInput
objectName: "amountToSendInput"
caption: root.caption
interactive: true
selectedHolding: d.selectedHolding
fiatInputInteractive: root.fiatInputInteractive
multiplierIndex: d.isSelectedHoldingValidAsset && !!holdingSelector.selectedItem && !!holdingSelector.selectedItem.decimals
? holdingSelector.selectedItem.decimals
: 0
maxInputBalance: (root.swapSide === SwapInputPanel.SwapSide.Receive || !d.isSelectedHoldingValidAsset) ? Number.POSITIVE_INFINITY
: d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
currentCurrency: root.currencyStore.currentCurrency
formatCurrencyAmount: root.currencyStore.formatCurrencyAmount
loading: root.loading
}
}
ColumnLayout {
Layout.preferredWidth: parent.width*.33
Item { Layout.fillHeight: true }
HoldingSelector {
id: holdingSelector
objectName: "holdingSelector"
Layout.rightMargin: d.isSelectedHoldingValidAsset ? -root.padding : 0
Layout.alignment: Qt.AlignRight
Layout.preferredHeight: 38
searchPlaceholderText: qsTr("Search asset name or symbol")
assetsModel: SortFilterProxyModel {
sourceModel: root.processedAssetsModel
filters: FastExpressionFilter {
function search(symbol, name, searchString) {
return (symbol.toUpperCase().includes(searchString.toUpperCase())
|| name.toUpperCase().includes(searchString.toUpperCase()))
}
expression: search(model.symbol, model.name, d.searchText)
expectedRoles: ["symbol", "name"]
}
}
networksModel: root.flatNetworksModel
formatCurrentCurrencyAmount: function(balance) {
return root.currencyStore.formatCurrencyAmount(balance, root.currencyStore.currentCurrency)
}
formatCurrencyAmountFromBigInt: function(balance, symbol, decimals) {
return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
}
onItemSelected: {
d.setSelectedHoldingId(holdingId, holdingType)
amountToSendInput.input.forceActiveFocus()
}
onSearchTextChanged: d.searchText = searchText
}
Item { Layout.fillHeight: !itemTag.visible }
StatusListItemTag {
id: itemTag
objectName: "maxTagButton"
Layout.alignment: Qt.AlignRight
Layout.maximumWidth: parent.width
Layout.preferredHeight: 22
visible: d.isSelectedHoldingValidAsset && root.swapSide === SwapInputPanel.SwapSide.Pay
title: d.maxInputBalance > 0 ? qsTr("Max: %1").arg(d.maxInputBalanceFormatted)
: qsTr("No balances active")
tagClickable: true
closeButtonVisible: false
titleText.font.pixelSize: 12
bgColor: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor3 : Theme.palette.dangerColor2
titleText.color: amountToSendInput.input.valid || !amountToSendInput.input.text ? Theme.palette.primaryColor1 : Theme.palette.dangerColor1
onTagClicked: {
const max = d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
if (max > 0)
amountToSendInput.input.text = max.toLocaleString(Qt.locale(), 'f', -128)
else
amountToSendInput.input.input.edit.clear()
amountToSendInput.input.forceActiveFocus()
}
}
}
}
}