feat: [UI - Swap] Create swap input component

- new panel created: `SwapInputPanel`
- some cleanups to the needed stores
- created a SB page demonstrating the use of 2 panels and the
`SwapExchangeButton`
- created QML tests

Fixes #14781
This commit is contained in:
Lukáš Tinkl 2024-05-28 19:39:41 +02:00 committed by Lukáš Tinkl
parent a7b9a62745
commit a3c9012f4a
36 changed files with 1045 additions and 227 deletions

View File

@ -38,7 +38,7 @@ SplitView {
maxInputBalance: inputIsFiat ? root.maxCryptoBalance*amountToSendInput.selectedHolding.marketDetails.currencyPrice.amount
: root.maxCryptoBalance
currentCurrency: "Fiat"
currentCurrency: "USD"
formatCurrencyAmount: function(amount, symbol, options, locale) {
const currencyAmount = {
amount: amount,
@ -57,47 +57,42 @@ SplitView {
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.minimumHeight: 250
logsView.logText: logs.logText
}
}
Pane {
SplitView.minimumWidth: 300
SplitView.preferredWidth: 300
ColumnLayout {
Label {
Layout.topMargin: 10
Layout.fillWidth: true
text: "Max Crypto Balance"
}
ColumnLayout {
Label {
Layout.topMargin: 10
Layout.fillWidth: true
text: "Max Crypto Balance"
}
TextField {
id: maxCryptoBalanceText
background: Rectangle { border.color: 'lightgrey' }
Layout.preferredWidth: 200
text: "1000000"
}
TextField {
id: maxCryptoBalanceText
background: Rectangle { border.color: 'lightgrey' }
Layout.preferredWidth: 200
text: "1000000"
}
Label {
Layout.topMargin: 10
Layout.fillWidth: true
text: "Decimals"
}
Label {
Layout.topMargin: 10
Layout.fillWidth: true
text: "Decimals"
}
TextField {
id: decimalsText
background: Rectangle { border.color: 'lightgrey' }
Layout.preferredWidth: 200
text: "6"
}
TextField {
id: decimalsText
background: Rectangle { border.color: 'lightgrey' }
Layout.preferredWidth: 200
text: "6"
}
CheckBox {
id: fiatInput
CheckBox {
id: fiatInput
text: "Fiat input value"
text: "Fiat input value"
}
}
}
}

View File

@ -47,7 +47,7 @@ SplitView {
ColumnLayout {
Repeater {
model: [0.1, 0.5, 0.24, 0.8, 120.84]
model: [0, 0.1, 0.5, 0.24, 0.8, 120.84]
Button {
text: "set " + modelData

View File

@ -0,0 +1,203 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
import shared.stores 1.0
import shared.stores.send 1.0
import AppLayouts.Wallet.stores 1.0
import AppLayouts.Wallet.panels 1.0
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.popups.swap 1.0
import Models 1.0
import Storybook 1.0
import SortFilterProxyModel 0.2
SplitView {
id: root
Logs { id: logs }
QtObject {
id: d
readonly property SwapInputParamsForm swapInputParamsForm: SwapInputParamsForm {
fromTokensKey: ctrlFromTokensKey.text
fromTokenAmount: ctrlFromTokenAmount.text
toTokenKey: ctrlToTokenKey.text
toTokenAmount: ctrlToTokenAmount.text
}
readonly property SwapModalAdaptor adaptor: SwapModalAdaptor {
swapStore: SwapStore {
readonly property var accounts: WalletAccountsModel {}
readonly property var flatNetworks: NetworksModel.flatNetworks
readonly property bool areTestNetworksEnabled: false
}
walletAssetsStore: WalletAssetsStore {
id: thisWalletAssetStore
walletTokensStore: TokensStore {
plainTokensBySymbolModel: TokensBySymbolModel {}
}
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
}
currencyStore: CurrenciesStore {}
swapFormData: d.swapInputParamsForm
}
}
Rectangle {
SplitView.fillWidth: true
SplitView.fillHeight: true
color: Theme.palette.baseColor3
Item {
width: 492
height: payPanel.height + receivePanel.height + 4
anchors.centerIn: parent
SwapInputPanel {
id: payPanel
anchors {
left: parent.left
right: parent.right
top: parent.top
}
currencyStore: d.adaptor.currencyStore
flatNetworksModel: d.adaptor.filteredFlatNetworksModel
processedAssetsModel: d.adaptor.processedAssetsModel
tokenKey: d.swapInputParamsForm.fromTokensKey
tokenAmount: d.swapInputParamsForm.fromTokenAmount
swapSide: SwapInputPanel.SwapSide.Pay
fiatInputInteractive: ctrlFiatInputInteractive.checked
swapExchangeButtonWidth: swapButton.width
loading: ctrlLoading.checked
}
SwapInputPanel {
id: receivePanel
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
currencyStore: d.adaptor.currencyStore
flatNetworksModel: d.adaptor.filteredFlatNetworksModel
processedAssetsModel: d.adaptor.processedAssetsModel
tokenKey: d.swapInputParamsForm.toTokenKey
tokenAmount: d.swapInputParamsForm.toTokenAmount
swapSide: SwapInputPanel.SwapSide.Receive
fiatInputInteractive: ctrlFiatInputInteractive.checked
swapExchangeButtonWidth: swapButton.width
loading: ctrlLoading.checked
}
SwapExchangeButton {
id: swapButton
anchors.centerIn: parent
}
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumWidth: 250
SplitView.preferredWidth: 250
logsView.logText: logs.logText
ColumnLayout {
anchors.fill: parent
RowLayout {
Layout.fillWidth: true
Label {
text: "Pay symbol:"
}
TextField {
Layout.fillWidth: true
id: ctrlFromTokensKey
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Pay amount:"
}
TextField {
Layout.fillWidth: true
id: ctrlFromTokenAmount
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Receive symbol:"
}
TextField {
Layout.fillWidth: true
id: ctrlToTokenKey
text: "STT"
}
}
RowLayout {
Layout.fillWidth: true
Label {
text: "Receive amount:"
}
TextField {
Layout.fillWidth: true
id: ctrlToTokenAmount
}
}
Switch {
id: ctrlFiatInputInteractive
text: "Fiat input interactive"
checked: false
}
Switch {
id: ctrlLoading
text: "Loading"
}
Label {
Layout.fillWidth: true
font.weight: Font.Medium
text: "<b>Pay:</b><ul><li>Symbol: %1<li>Amount: %2<li>Valid: %3"
.arg(payPanel.selectedHoldingId || "N/A")
.arg(payPanel.cryptoValue.toString())
.arg(payPanel.cryptoValueValid ? "true" : "false")
}
Label {
Layout.fillWidth: true
font.weight: Font.Medium
text: "<b>Receive:</b><ul><li>Symbol: %1<li>Amount: %2<li>Valid: %3"
.arg(receivePanel.selectedHoldingId || "N/A")
.arg(receivePanel.cryptoValue.toString())
.arg(receivePanel.cryptoValueValid ? "true" : "false")
}
Item { Layout.fillHeight: true }
}
}
}
// category: Panels
// https://www.figma.com/design/TS0eQX9dAZXqZtELiwKIoK/Swap---Milestone-1?node-id=3404-111405&t=G96tBLQr2j73HT9X-0

View File

@ -170,7 +170,7 @@ SplitView {
StatusInput {
id: swapInput
Layout.preferredWidth: 100
label: "Token mount to swap"
label: "Token amount to swap"
text: "100"
}

View File

@ -51,9 +51,7 @@ SplitView {
return currencyStore.formatCurrencyAmount(balance, "USD")
}
formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){
let bigIntBalance = AmountsArithmetic.fromString(balance)
let decimalBalance = AmountsArithmetic.toNumber(bigIntBalance, decimals)
return currencyStore.formatCurrencyAmount(decimalBalance, symbol)
return currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
}
}
}

View File

@ -10,6 +10,10 @@ class Setup : public QObject
public slots:
void qmlEngineAvailable(QQmlEngine *engine) {
// custom code that needs QQmlEngine, register QML types, add import paths,...
QGuiApplication::setOrganizationName(QStringLiteral("Status"));
QGuiApplication::setOrganizationDomain(QStringLiteral("status.im"));
const QStringList additionalImportPaths {
STATUSQ_MODULE_IMPORT_PATH,
QML_IMPORT_ROOT + QStringLiteral("/../ui/app"),

View File

@ -80,6 +80,7 @@ Item {
keyClick(Qt.Key_3)
compare(controlUnderTest.text, "13")
compare(controlUnderTest.valid, true)
}
function test_defaultValidation() {

View File

@ -0,0 +1,252 @@
import QtQuick 2.15
import QtTest 1.15
import StatusQ 0.1
import AppLayouts.Wallet.stores 1.0
import AppLayouts.Wallet.panels 1.0
import AppLayouts.Wallet.popups.swap 1.0
import shared.stores 1.0
import SortFilterProxyModel 0.2
import Models 1.0
import Storybook 1.0
Item {
id: root
width: 600
height: 400
QtObject {
id: d
readonly property SwapModalAdaptor adaptor: SwapModalAdaptor {
swapStore: SwapStore {
readonly property var accounts: WalletAccountsModel {}
readonly property var flatNetworks: NetworksModel.flatNetworks
readonly property bool areTestNetworksEnabled: false
}
walletAssetsStore: WalletAssetsStore {
id: thisWalletAssetStore
walletTokensStore: TokensStore {
plainTokensBySymbolModel: TokensBySymbolModel {}
}
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel
}
currencyStore: CurrenciesStore {}
swapFormData: SwapInputParamsForm {}
}
}
Component {
id: componentUnderTest
SwapInputPanel {
anchors.centerIn: parent
currencyStore: d.adaptor.currencyStore
flatNetworksModel: d.adaptor.filteredFlatNetworksModel
processedAssetsModel: d.adaptor.processedAssetsModel
}
}
property SwapInputPanel controlUnderTest: null
TestCase {
name: "SwapInputPanel"
when: windowShown
function test_basicSetupAndDefaults() {
controlUnderTest = createTemporaryObject(componentUnderTest, root)
verify(!!controlUnderTest)
verify(controlUnderTest.width > 0)
verify(controlUnderTest.height > 0)
tryCompare(controlUnderTest, "swapSide", SwapInputPanel.SwapSide.Pay)
tryCompare(controlUnderTest, "caption", qsTr("Pay"))
tryCompare(controlUnderTest, "selectedHoldingId", "")
tryCompare(controlUnderTest, "cryptoValue", 0)
tryCompare(controlUnderTest, "cryptoValueRaw", "0")
}
function test_basicSetupReceiveSide() {
controlUnderTest = createTemporaryObject(componentUnderTest, root, {swapSide: SwapInputPanel.SwapSide.Receive})
verify(!!controlUnderTest)
verify(controlUnderTest.width > 0)
verify(controlUnderTest.height > 0)
tryCompare(controlUnderTest, "swapSide", SwapInputPanel.SwapSide.Receive)
tryCompare(controlUnderTest, "caption", qsTr("Receive"))
tryCompare(controlUnderTest, "selectedHoldingId", "")
tryCompare(controlUnderTest, "cryptoValue", 0)
tryCompare(controlUnderTest, "cryptoValueRaw", "0")
}
function test_basicSetupWithInitialProperties() {
controlUnderTest = createTemporaryObject(componentUnderTest, root,
{
swapSide: SwapInputPanel.SwapSide.Pay,
tokenKey: "STT",
tokenAmount: "10000000.0000001"
})
verify(!!controlUnderTest)
waitForRendering(controlUnderTest)
tryCompare(controlUnderTest, "swapSide", SwapInputPanel.SwapSide.Pay)
tryCompare(controlUnderTest, "selectedHoldingId", "STT")
tryCompare(controlUnderTest, "cryptoValue", 10000000.0000001)
verify(controlUnderTest.cryptoValueValid)
}
function test_setTokenKeyAndAmounts_data() {
return [
{ tag: "1.42", tokenAmount: "1.42", valid: true },
{ tag: "0.00001", tokenAmount: "0.00001", valid: true },
{ tag: "1234567890", tokenAmount: "1234567890", valid: true },
{ tag: "1234567890.1234567890", tokenAmount: "1234567890.1234567890", valid: true },
{ tag: "abc", tokenAmount: "abc", valid: false },
{ tag: "NaN", tokenAmount: "NaN", valid: false }
]
}
function test_setTokenKeyAndAmounts(data) {
const valid = data.valid
const tokenAmount = data.tokenAmount
const tokenSymbol = "STT"
controlUnderTest = createTemporaryObject(componentUnderTest, root)
verify(!!controlUnderTest)
controlUnderTest.tokenKey = tokenSymbol
controlUnderTest.tokenAmount = tokenAmount
tryCompare(controlUnderTest, "selectedHoldingId", tokenSymbol)
if (!valid)
expectFail(data.tag, "Invalid data expected to fail: %1".arg(tokenAmount))
tryCompare(controlUnderTest, "cryptoValue", parseFloat(tokenAmount))
tryCompare(controlUnderTest, "cryptoValueValid", true)
const holdingSelector = findChild(controlUnderTest, "holdingSelector")
verify(!!holdingSelector)
tryCompare(holdingSelector.selectedItem, "symbol", tokenSymbol)
const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
verify(!!amountToSendInput)
tryCompare(amountToSendInput.input, "text", Number(tokenAmount).toLocaleString(Qt.locale(), 'f', -128))
}
function test_enterTokenAmountLocalizedNumber() {
controlUnderTest = createTemporaryObject(componentUnderTest, root, {tokenKey: "STT"})
verify(!!controlUnderTest)
waitForRendering(controlUnderTest)
tryCompare(controlUnderTest, "selectedHoldingId", "STT")
const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
verify(!!amountToSendInput)
mouseClick(amountToSendInput)
waitForRendering(amountToSendInput)
verify(amountToSendInput.input.input.edit.activeFocus)
amountToSendInput.input.locale = Qt.locale("cs_CZ")
compare(amountToSendInput.input.locale.name, "cs_CZ")
// manually entering "1000000,00000042" meaning "1000000,00000042"; `,` being the decimal separator
keyClick(Qt.Key_1)
for (let i = 0; i < 6; i++)
keyClick(Qt.Key_0)
keyClick(Qt.Key_Comma)
for (let i = 0; i < 6; i++)
keyClick(Qt.Key_0)
keyClick(Qt.Key_4)
keyClick(Qt.Key_2)
tryCompare(amountToSendInput.input, "text", "1000000,00000042")
tryCompare(controlUnderTest, "cryptoValue", 1000000.00000042)
verify(controlUnderTest.cryptoValueValid)
}
function test_selectSTTHoldingAndTypeAmount() {
controlUnderTest = createTemporaryObject(componentUnderTest, root)
verify(!!controlUnderTest)
const holdingSelector = findChild(controlUnderTest, "holdingSelector")
verify(!!holdingSelector)
mouseClick(holdingSelector)
waitForRendering(holdingSelector)
const assetSelectorList = findChild(holdingSelector, "assetSelectorList")
verify(!!assetSelectorList)
waitForRendering(assetSelectorList)
const sttDelegate = findChild(assetSelectorList, "AssetSelector_ItemDelegate_STT")
verify(!!sttDelegate)
mouseClick(sttDelegate, 40, 40) // center might be covered by tags
tryCompare(controlUnderTest, "selectedHoldingId", "STT")
const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
verify(!!amountToSendInput)
mouseClick(amountToSendInput)
waitForRendering(amountToSendInput)
verify(amountToSendInput.input.input.edit.activeFocus)
keyClick(Qt.Key_1)
keyClick(Qt.Key_Period)
keyClick(Qt.Key_4)
keyClick(Qt.Key_2)
tryCompare(controlUnderTest, "cryptoValue", 1.42)
verify(controlUnderTest.cryptoValueValid)
}
function test_clickingMaxButton() {
controlUnderTest = createTemporaryObject(componentUnderTest, root, {tokenKey: "ETH"})
verify(!!controlUnderTest)
waitForRendering(controlUnderTest)
tryCompare(controlUnderTest, "selectedHoldingId", "ETH")
const maxTagButton = findChild(controlUnderTest, "maxTagButton")
verify(!!maxTagButton)
waitForRendering(maxTagButton)
verify(maxTagButton.visible)
mouseClick(maxTagButton)
const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
verify(!!amountToSendInput)
waitForRendering(amountToSendInput)
const maxValue = amountToSendInput.maxInputBalance
tryCompare(amountToSendInput.input, "text", maxValue.toLocaleString(Qt.locale(), 'f', -128))
tryCompare(controlUnderTest, "cryptoValue", maxValue)
verify(controlUnderTest.cryptoValueValid)
}
function test_loadingState() {
controlUnderTest = createTemporaryObject(componentUnderTest, root)
verify(!!controlUnderTest)
controlUnderTest.loading = true
const amountToSendInput = findChild(controlUnderTest, "amountToSendInput")
verify(!!amountToSendInput)
const amountInput = findChild(amountToSendInput, "amountInput")
verify(!!amountInput)
verify(!amountInput.visible)
const topAmountToSendInputLoadingComponent = findChild(amountToSendInput, "topAmountToSendInputLoadingComponent")
verify(!!topAmountToSendInputLoadingComponent)
verify(topAmountToSendInputLoadingComponent.visible)
const bottomItemText = findChild(amountToSendInput, "bottomItemText")
verify(!!bottomItemText)
verify(!bottomItemText.visible)
const bottomItemTextLoadingComponent = findChild(amountToSendInput, "bottomItemTextLoadingComponent")
verify(!!bottomItemTextLoadingComponent)
verify(bottomItemTextLoadingComponent.visible)
}
}
}

View File

@ -33,7 +33,7 @@ Item {
walletAssetsStore: WalletAssetsStore {
id: thisWalletAssetStore
walletTokensStore: TokensStore {
readonly property var plainTokensBySymbolModel: TokensBySymbolModel {}
plainTokensBySymbolModel: TokensBySymbolModel {}
}
readonly property var baseGroupedAccountAssetModel: GroupedAccountsAssetsModel {}
assetsWithFilteredBalances: thisWalletAssetStore.groupedAccountsAssetsModel

View File

@ -1,4 +1,4 @@
import QtQuick 2.15
import QtQml 2.15
QtObject {
id: root

View File

@ -3,6 +3,8 @@ import QtQuick 2.15
QtObject {
id: root
property bool displayAssetsBelowBalance: false
property var plainTokensBySymbolModel
property bool displayAssetsBelowBalance
property var getDisplayAssetsBelowBalanceThresholdDisplayAmount
property double tokenListUpdatedAt
}

View File

@ -8,7 +8,7 @@ import Models 1.0
QtObject {
id: root
property TokensStore walletTokensStore
property TokensStore walletTokensStore: TokensStore {}
readonly property var groupedAccountsAssetsModel: GroupedAccountsAssetsModel {}
property var assetsWithFilteredBalances
@ -56,5 +56,11 @@ QtObject {
joinRole: "communityId"
}
property var assetsController
property var assetsController: QtObject {
property int revision
function filterAcceptsSymbol(symbol) {
return true
}
}
}

View File

@ -1,6 +1,7 @@
import QtQuick 2.15
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
QtObject {
id: root
@ -16,6 +17,12 @@ QtObject {
return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale)
}
function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
let bigIntBalance = SQUtils.AmountsArithmetic.fromString(balance)
let decimalBalance = SQUtils.AmountsArithmetic.toNumber(bigIntBalance, decimals)
return formatCurrencyAmount(decimalBalance, symbol)
}
function getFiatValue(balance, cryptoSymbol) {
return balance
}

View File

@ -12,7 +12,7 @@ import AppLayouts.Wallet.stores 1.0
QtObject {
id: root
readonly property var currencyStore: CurrenciesStore{}
readonly property CurrenciesStore currencyStore: CurrenciesStore {}
readonly property var senderAccounts: WalletSendAccountsModel {
Component.onCompleted: selectedSenderAccount = senderAccounts.get(0)
}
@ -280,7 +280,7 @@ QtObject {
},
FastExpressionRole {
name: "currentBalance"
expression: __getTotalBalance(model.balances, model.decimals, model.symbol, root.selectedSenderAccount)
expression: __getTotalBalance(model.balances, model.decimals)
expectedRoles: ["balances", "decimals", "symbol"]
},
FastExpressionRole {
@ -302,7 +302,7 @@ QtObject {
name.toUpperCase().startsWith(searchString.toUpperCase()) || __searchAddressInList(addressPerChain, searchString)
)
}
expression: search(symbol, name, addressPerChain, root.assetSearchString)
expression: search(model.symbol, model.name, model.addressPerChain, root.assetSearchString)
expectedRoles: ["symbol", "name", "addressPerChain"]
},
ValueFilter {
@ -339,7 +339,7 @@ QtObject {
}
/* Internal function to calculate total balance */
function __getTotalBalance(balances, decimals, symbol) {
function __getTotalBalance(balances, decimals) {
let totalBalance = 0
for(let i=0; i<balances.count; i++) {
let balancePerAddressPerChain = SQUtils.ModelUtils.get(balances, i)

View File

@ -94,7 +94,9 @@ Rectangle {
property alias subTitleBadgeComponent: subTitleBadgeLoader.sourceComponent
property alias errorIcon: errorIcon
property alias statusListItemTagsRowLayout: statusListItemSubtitleTagsRow
property bool showLoadingIndicator: false
property bool tagsScrollBarVisible: true
property int subTitleBadgeLoaderAlignment: Qt.AlignVCenter
@ -391,6 +393,7 @@ Rectangle {
width: Math.min(statusListItemTagsSlotInline.width, statusListItemTagsSlotInline.availableWidth, parent.width)
height: visible ? contentHeight : 0
padding: 0
ScrollBar.horizontal.policy: root.tagsScrollBarVisible ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
Row {
id: statusListItemTagsSlotInline
@ -399,7 +402,7 @@ Rectangle {
Repeater {
id: tagsRepeater
delegate: tagsDelegate
delegate: root.tagsDelegate
}
}
}

View File

@ -81,7 +81,6 @@ Control {
icon: "close-circle"
visible: closeButtonVisible
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor

View File

@ -21,15 +21,17 @@ StatusInput {
}
]
onKeyPressed: (event) => {
// additionally accept dot (.) and convert it to the correct decimal point char
if (event.key === Qt.Key_Period || event.key === Qt.Key_Comma) {
// Only one decimal point is allowed
if(root.text.indexOf(root.locale.decimalPoint) === -1)
root.input.insert(root.input.cursorPosition, root.locale.decimalPoint)
event.accepted = true
} else if ((event.key > Qt.Key_9 && event.key <= Qt.Key_BraceRight) || event.key === Qt.Key_Space || event.key === Qt.Key_Tab) {
event.accepted = true
}
}
onKeyPressed:
(event) => {
// additionally accept dot (.) and convert it to the correct decimal point char
if (event.key === Qt.Key_Period || event.key === Qt.Key_Comma) {
// Only one decimal point is allowed
if(root.text.indexOf(root.locale.decimalPoint) === -1) {
root.input.insert(root.input.cursorPosition, root.locale.decimalPoint)
event.accepted = true
}
} else if (event.modifiers === Qt.NoModifier && ((event.key > Qt.Key_9 && event.key <= Qt.Key_BraceRight) || event.key === Qt.Key_Space || event.key === Qt.Key_Tab)) {
event.accepted = true
}
}
}

View File

@ -400,7 +400,7 @@ Item {
KeyNavigation.tab: root.tabNavItem
Keys.onPressed: {
edit.keyEvent = event.key
root.keyPressed(event);
root.keyPressed(event)
}
onCursorRectangleChanged: Utils.ensureVisible(flick, cursorRectangle)
onActiveFocusChanged: if (root.pristine) root.pristine = false

View File

@ -72,6 +72,7 @@ QtObject {
function stripTrailingZeroes(numStr, locale) {
locale = locale || Qt.locale()
let regEx = locale.decimalPoint == "." ? /(\.[0-9]*[1-9])0+$|\.0*$/ : /(\,[0-9]*[1-9])0+$|\,0*$/
return numStr.replace(regEx, '$1')
}
@ -157,10 +158,10 @@ QtObject {
var optDisplayDecimals = currencyAmount.displayDecimals
var optStripTrailingZeroes = currencyAmount.stripTrailingZeroes
if (options) {
if (options.noSymbol !== undefined) {
if (options.noSymbol !== undefined && options.noSymbol === true) {
optNoSymbol = true
}
if (options.rawAmount !== undefined) {
if (options.rawAmount !== undefined && options.rawAmount === true) {
optRawAmount = true
}
if (options.minDecimals !== undefined && options.minDecimals > optDisplayDecimals) {
@ -175,8 +176,7 @@ QtObject {
var amountSuffix = ""
let minAmount = 10**-optDisplayDecimals
if (currencyValue > 0 && currencyValue < minAmount && !optRawAmount)
{
if (currencyValue > 0 && currencyValue < minAmount && !optRawAmount) {
// Handle amounts smaller than resolution
amountStr = "<%1".arg(numberToLocaleString(minAmount, optDisplayDecimals, locale))
} else {

View File

@ -2,7 +2,6 @@ import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
StatusButton {
@ -12,6 +11,7 @@ StatusButton {
icon.name: hovered ? "arrow-up" : "arrow-down"
icon.color: Theme.palette.baseColor1
focusPolicy: Qt.NoFocus
isRoundIcon: true
radius: height/2
normalColor: Theme.palette.indirectColor3

View File

@ -0,0 +1,277 @@
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()
}
}
}
}
}

View File

@ -5,4 +5,5 @@ ActivityFilterPanel 1.0 ActivityFilterPanel.qml
ManageAssetsPanel 1.0 ManageAssetsPanel.qml
ManageCollectiblesPanel 1.0 ManageCollectiblesPanel.qml
ManageHiddenPanel 1.0 ManageHiddenPanel.qml
DAppsWorkflow 1.0 DAppsWorkflow.qml
DAppsWorkflow 1.0 DAppsWorkflow.qml
SwapInputPanel 1.0 SwapInputPanel.qml

View File

@ -1,6 +1,4 @@
import QtQuick 2.13
import utils 1.0
import QtQml 2.15
/* This is used so that there is an easy way to fill in the data
needed to launch the Swap Modal with pre-filled requisites. */
@ -11,5 +9,5 @@ QtObject {
property string fromTokensKey: ""
property string fromTokenAmount: ""
property string toTokenKey: ""
property string toTokenAmount
}

View File

@ -1,4 +1,4 @@
import QtQuick 2.13
import QtQuick 2.15
import QtQuick.Layouts 1.15
import utils 1.0

View File

@ -55,8 +55,12 @@ QObject {
return networkString
}
function formatCurrencyAmount(balance, symbol) {
return root.currencyStore.formatCurrencyAmount(balance, symbol)
function formatCurrencyAmount(balance, symbol, options = null, locale = null) {
return root.currencyStore.formatCurrencyAmount(balance, symbol, options, locale)
}
function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
return root.currencyStore.formatCurrencyAmountFromBigInt(balance, symbol, decimals)
}
// TODO: remove once the AccountsModalHeader is reworked!!
@ -67,9 +71,80 @@ QObject {
return null
}
// Model prepared to provide filtered and sorted assets as per the advanced Settings in token management
readonly property var processedAssetsModel: SortFilterProxyModel {
property real displayAssetsBelowBalanceThresholdAmount: root.walletAssetsStore.walletTokensStore.getDisplayAssetsBelowBalanceThresholdDisplayAmount()
sourceModel: __assetsWithFilteredBalances
proxyRoles: [
FastExpressionRole {
name: "isCommunityAsset"
expression: !!model.communityId
expectedRoles: ["communityId"]
},
FastExpressionRole {
name: "currentBalance"
expression: __getTotalBalance(model.balances, model.decimals)
expectedRoles: ["balances", "decimals"]
},
FastExpressionRole {
name: "currentCurrencyBalance"
expression: {
if (!!model.marketDetails) {
return model.currentBalance * model.marketDetails.currencyPrice.amount
}
return 0
}
expectedRoles: ["marketDetails", "currentBalance"]
}
]
filters: [
FastExpressionFilter {
expression: {
root.walletAssetsStore.assetsController.revision
if (!root.walletAssetsStore.assetsController.filterAcceptsSymbol(model.symbol)) // explicitely hidden
return false
if (model.isCommunityAsset) // do not show community assets
return false
if (root.walletAssetsStore.walletTokensStore.displayAssetsBelowBalance)
return model.currentCurrencyBalance > processedAssetsModel.displayAssetsBelowBalanceThresholdAmount
return true
}
expectedRoles: ["symbol", "isCommunityAsset", "currentCurrencyBalance"]
}
]
// FIXME sort by assetsController instead, to have the sorting/order as in the main wallet view
// sorters: RoleSorter {
// roleName: "isCommunityAsset"
// }
}
// Internal properties and functions -----------------------------------------------------------------------------------------------------------------------------
readonly property var __fromToken: ModelUtils.getByKey(root.walletAssetsStore.walletTokensStore.plainTokensBySymbolModel, "key", root.swapFormData.fromTokensKey)
// Internal model filtering balances by the account selected in the AccountsModalHeader
SubmodelProxyModel {
id: __assetsWithFilteredBalances
sourceModel: root.walletAssetsStore.groupedAccountAssetsModel
submodelRoleName: "balances"
delegateModel: SortFilterProxyModel {
sourceModel: submodel
filters: [
ValueFilter {
roleName: "chainId"
value: root.swapFormData.selectedNetworkChainId
enabled: root.swapFormData.selectedNetworkChainId !== -1
}/*,
// TODO enable once AccountsModalHeader is reworked!!
ValueFilter {
roleName: "account"
value: root.selectedSenderAccount.address
}*/
]
}
}
SubmodelProxyModel {
id: filteredBalancesModel
sourceModel: root.walletAssetsStore.baseGroupedAccountAssetModel
@ -104,4 +179,14 @@ QObject {
}
return null
}
/* Internal function to calculate total balance */
function __getTotalBalance(balances, decimals) {
let totalBalance = 0
for(let i=0; i<balances.count; i++) {
let balancePerAddressPerChain = ModelUtils.get(balances, i)
totalBalance+=AmountsArithmetic.toNumber(balancePerAddressPerChain.balance, decimals)
}
return totalBalance
}
}

View File

@ -18,12 +18,13 @@ QtObject {
ex. uniswap list, status tokens list */
readonly property var sourcesOfTokensModel: SortFilterProxyModel {
sourceModel: !!root._allTokensModule ? root._allTokensModule.sourcesOfTokensModel : null
proxyRoles: ExpressionRole {
proxyRoles: FastExpressionRole {
function sourceImage(sourceKey) {
return Constants.getSupportedTokenSourceImage(sourceKey)
}
name: "image"
expression: sourceImage(model.key)
expectedRoles: ["key"]
}
filters: AnyOf {
ValueFilter {
@ -56,16 +57,18 @@ QtObject {
sourceModel: root._joinFlatTokensModel
proxyRoles: [
ExpressionRole {
FastExpressionRole {
name: "explorerUrl"
expression: model.blockExplorerURL + "/token/" + model.address
expectedRoles: ["blockExplorerURL", "address"]
},
ExpressionRole {
FastExpressionRole {
function tokenIcon(symbol) {
return Constants.tokenIcon(symbol)
}
name: "image"
expression: tokenIcon(model.symbol)
expectedRoles: ["symbol"]
}
]
}
@ -79,24 +82,27 @@ QtObject {
readonly property var assetsBySymbolModel: SortFilterProxyModel {
sourceModel: plainTokensBySymbolModel
proxyRoles: [
ExpressionRole {
FastExpressionRole {
function tokenIcon(symbol) {
return Constants.tokenIcon(symbol)
}
name: "iconSource"
expression: tokenIcon(model.symbol)
expectedRoles: ["symbol"]
},
// TODO: Review if it can be removed
ExpressionRole {
FastExpressionRole {
name: "shortName"
expression: model.symbol
expectedRoles: ["symbol"]
},
ExpressionRole {
FastExpressionRole {
function getCategory(index) {
return 0
}
name: "category"
expression: getCategory(model.communityId)
expectedRoles: ["communityId"]
}
]
}

View File

@ -77,6 +77,7 @@ Item {
readonly property TransactionStore transactionStore: TransactionStore {
walletAssetStore: appMain.walletAssetsStore
tokensStore: appMain.tokensStore
currencyStore: appMain.currencyStore
}
// set from main.qml

View File

@ -175,7 +175,7 @@ StatusDialog {
}
if(!!popup.preDefinedAmountToSend) {
amountToSendInput.input.text = popup.preDefinedAmountToSend
amountToSendInput.input.text = Number(popup.preDefinedAmountToSend).toLocaleString(Qt.locale(), 'f', -128)
}
if(!!popup.preSelectedRecipient) {
@ -261,11 +261,9 @@ StatusDialog {
id: holdingSelector
Layout.fillWidth: true
Layout.fillHeight: true
selectedSenderAccount: store.selectedSenderAccount.address
assetsModel: popup.store.processedAssetsModel
collectiblesModel: popup.preSelectedAccount ? popup.nestedCollectiblesModel : null
networksModel: popup.store.flatNetworksModel
currentCurrencySymbol: d.currencyStore.currentCurrencySymbol
visible: (!!d.selectedHolding && d.selectedHoldingType !== Constants.TokenType.Unknown) ||
(!!d.hoveredHolding && d.hoveredHoldingType !== Constants.TokenType.Unknown)
onItemSelected: {
@ -291,7 +289,8 @@ StatusDialog {
const max = d.prepareForMaxSend(input, d.hoveredHolding.symbol)
if (max <= 0)
return qsTr("No balances active")
const balance = d.currencyStore.formatCurrencyAmount(max , d.hoveredHolding.symbol)
const balance = d.currencyStore.formatCurrencyAmount(max, amountToSendInput.inputIsFiat ? amountToSendInput.currentCurrency
: d.selectedHolding.symbol)
return qsTr("Max: %1").arg(balance.toString())
}
const max = d.prepareForMaxSend(d.maxInputBalance, d.inputSymbol)
@ -394,7 +393,6 @@ StatusDialog {
Layout.bottomMargin: Style.current.xlPadding
visible: !d.selectedHolding
selectedSenderAccount: store.selectedSenderAccount.address
assets: popup.store.processedAssetsModel
collectibles: popup.preSelectedAccount ? popup.nestedCollectiblesModel : null
networksModel: popup.store.flatNetworksModel
@ -528,4 +526,3 @@ StatusDialog {
}
}
}

View File

@ -16,7 +16,6 @@ StatusAmountInput {
bottomPadding: 0
placeholderText: ""
input.edit.cursorVisible: true
input.edit.font.pixelSize: Utils.getFontSizeBasedOnLetterCount(text)
input.placeholderFont.pixelSize: 34
input.edit.padding: 0

View File

@ -19,15 +19,10 @@ StatusListItem {
property var formatCurrentCurrencyAmount: function(balance){}
property var formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){}
property var balancesModel
property string selectedSenderAccount
QtObject {
id: d
readonly property int indexesThatCanBeShown:
Math.floor((root.statusListItemInlineTagsSlot.availableWidth
- compactRow.width) / statusListItemInlineTagsSlot.children[0].width) - 1
function selectToken() {
root.tokenSelected({name, symbol, balances, decimals})
}
@ -62,30 +57,13 @@ StatusListItem {
statusListItemInlineTagsSlot.spacing: 0
tagsModel: root.balancesModel
tagsDelegate: expandedItem
statusListItemInlineTagsSlot.children: Row {
id: compactRow
spacing: -6
Repeater {
model: root.balancesModel
delegate: compactItem
}
}
tagsScrollBarVisible: false
radius: sensor.containsMouse || root.highlighted ? 0 : 8
color: sensor.containsMouse || highlighted ? Theme.palette.baseColor2 : "transparent"
radius: sensor.containsMouse || highlighted ? 0 : 8
color: sensor.containsMouse || highlighted ? Theme.palette.statusListItem.highlightColor : "transparent"
onClicked: d.selectToken()
Component {
id: compactItem
StatusRoundedImage {
z: index + 1
width: 16
height: 16
image.source: Style.svg("tiny/%1".arg(model.iconUrl))
visible: !root.sensor.containsMouse || index > d.indexesThatCanBeShown
}
}
Component {
id: expandedItem
StatusListItemTag {
@ -98,8 +76,9 @@ StatusListItem {
asset.width: 16
asset.height: 16
asset.isImage: true
asset.name: Style.svg("tiny/%1".arg(iconUrl))
visible: root.sensor.containsMouse && index <= d.indexesThatCanBeShown
asset.name: Style.svg("tiny/%1".arg(model.iconUrl))
tagClickable: true
onTagClicked: d.selectToken()
}
}
}

View File

@ -1,17 +1,12 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import SortFilterProxyModel 0.2
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Backpressure 1.0
import shared.controls 1.0
import utils 1.0
Item {
@ -40,12 +35,6 @@ Item {
property int contentIconSize: 21
property int contentTextSize: 28
function resetInternal() {
items = null
selectedItem = null
hoveredItem = null
}
function openPopup() {
root.comboBoxControl.popup.open()
}
@ -68,9 +57,8 @@ Item {
property string iconSource: ""
onIconSourceChanged: tokenIcon.image.source = iconSource
property string text: ""
property string text: qsTr("Select asset")
readonly property bool isItemSelected: !!root.selectedItem || !!root.hoveredItem
}
StatusComboBox {
@ -80,9 +68,7 @@ Item {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, parent.width)
control.padding: 4
control.padding: 12
control.popup.width: 492
control.popup.x: -root.x
control.popup.verticalPadding: 0
@ -92,20 +78,21 @@ Item {
model: root.comboBoxModel
control.background: Rectangle {
color: "transparent"
color: !d.isItemSelected ? Theme.palette.primaryColor3 : "transparent"
border.width: d.isItemSelected ? 0 : 1
border.color: Theme.palette.directColor7
radius: 12
radius: 8
HoverHandler {
cursorShape: root.enabled ? Qt.PointingHandCursor : undefined
}
}
contentItem: RowLayout {
id: rowLayout
implicitHeight: 38
StatusRoundedImage {
id: tokenIcon
Layout.preferredWidth: root.contentIconSize
Layout.preferredHeight: root.contentIconSize
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
visible: !!d.iconSource
image.source: d.iconSource
image.onStatusChanged: {
@ -116,22 +103,17 @@ Item {
}
StatusBaseText {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
font.pixelSize: root.contentTextSize
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
color: Theme.palette.miscColor1
color: Theme.palette.primaryColor1
text: d.text
visible: d.isItemSelected
}
StatusIcon {
Layout.leftMargin: -3
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 16
Layout.preferredHeight: 16
icon: "chevron-down"
color: Theme.palette.miscColor1
visible: !!root.selectedItem
color: Theme.palette.primaryColor1
}
}

View File

@ -1,4 +1,3 @@
import QtQml 2.15
import QtQuick 2.15
import QtQuick.Layouts 1.15
@ -22,10 +21,8 @@ Item {
id: root
property var assetsModel
property string selectedSenderAccount
property var collectiblesModel
property var networksModel
property string currentCurrencySymbol
property bool onlyAssets: true
property string searchText
@ -41,6 +38,16 @@ Item {
property alias selectedItem: holdingItemSelector.selectedItem
property alias hoveredItem: holdingItemSelector.hoveredItem
property string searchPlaceholderText: {
if (d.isCurrentBrowsingTypeAsset) {
return qsTr("Search for token or enter token address")
} else if (d.isBrowsingGroup) {
return qsTr("Search %1").arg(d.currentBrowsingGroupName ?? qsTr("collectibles in collection"))
} else {
return qsTr("Search collectibles")
}
}
function setSelectedItem(item, holdingType) {
d.browsingHoldingType = holdingType
holdingItemSelector.selectedItem = null
@ -66,8 +73,8 @@ Item {
[qsTr("Assets")] :
[qsTr("Assets"), qsTr("Collectibles")]
readonly property var updateSearchText: Backpressure.debounce(root, 1000, function(inputText) {
searchText = inputText
readonly property var updateSearchText: Backpressure.debounce(root, 500, function(inputText) {
root.searchText = inputText
})
function isAsset(type) {
@ -103,10 +110,8 @@ Item {
} else if (asset.image) {
// Community assets have a dedicated image streamed from status-go
return asset.image
} else {
return Constants.tokenIcon(asset.symbol)
}
return ""
return Constants.tokenIcon(asset.symbol)
}
property var collectibleTextFn: function (item) {
@ -167,16 +172,6 @@ Item {
]
}
readonly property string searchPlaceholderText: {
if (isCurrentBrowsingTypeAsset) {
return qsTr("Search for token or enter token address")
} else if (isBrowsingGroup) {
return qsTr("Search %1").arg(d.currentBrowsingGroupName ?? qsTr("collectibles in collection"))
} else {
return qsTr("Search collectibles")
}
}
// By design values:
readonly property int padding: 16
readonly property int headerTopMargin: 5
@ -187,7 +182,6 @@ Item {
readonly property int collectibleContentIconSize: 28
readonly property int assetContentTextSize: 28
readonly property int collectibleContentTextSize: 15
}
HoldingItemSelector {
@ -246,6 +240,7 @@ Item {
contentIconSize: d.isAsset(d.currentHoldingType) ? d.assetContentIconSize : d.collectibleContentIconSize
contentTextSize: d.isAsset(d.currentHoldingType) ? d.assetContentTextSize : d.collectibleContentTextSize
comboBoxListViewSection.property: "isCommunityAsset"
// TODO allow for different header/sections for the Swap modal
comboBoxListViewSection.delegate: AssetsSectionDelegate {
height: !!text ? 52 : 0 // if we bind to some property instead of hardcoded value it wont work nice when switching tabs or going inside collection and back
width: ListView.view.width
@ -253,7 +248,10 @@ Item {
text: Helpers.assetsSectionTitle(section, holdingItemSelector.hasCommunityTokens, d.isBrowsingGroup, d.isCurrentBrowsingTypeAsset)
onOpenInfoPopup: Global.openPopup(communityInfoPopupCmp)
}
comboBoxControl.popup.onOpened: comboBoxControl.popup.contentItem.headerItem.focusSearch()
comboBoxControl.popup.onClosed: comboBoxControl.popup.contentItem.headerItem.clear()
comboBoxControl.popup.x: root.width - comboBoxControl.popup.width
}
Component {
@ -264,6 +262,10 @@ Item {
Component {
id: headerComponent
ColumnLayout {
function focusSearch() {
searchInput.input.forceActiveFocus()
}
function clear() {
searchInput.input.edit.clear()
}
@ -303,7 +305,7 @@ Item {
CollectibleBackButtonWithInfo {
Layout.fillWidth: true
visible: d.isBrowsingGroup
count: collectiblesModel.count
count: collectiblesModel ? collectiblesModel.count : 0
name: d.currentBrowsingGroupName
onBackClicked: {
if (!d.isCurrentBrowsingTypeAsset) {
@ -325,7 +327,7 @@ Item {
anchors.fill: parent
input.showBackground: false
placeholderText: d.searchPlaceholderText
placeholderText: root.searchPlaceholderText
onTextChanged: Qt.callLater(d.updateSearchText, text)
input.clearable: true
input.implicitHeight: 56
@ -344,7 +346,7 @@ Item {
TokenBalancePerChainDelegate {
objectName: "AssetSelector_ItemDelegate_" + symbol
width: holdingItemSelector.comboBoxControl.popup.width
selectedSenderAccount: root.selectedSenderAccount
highlighted: !!holdingItemSelector.selectedItem && symbol === holdingItemSelector.selectedItem.symbol
balancesModel: LeftJoinModel {
leftModel: balances
rightModel: root.networksModel
@ -370,6 +372,7 @@ Item {
CollectibleNestedDelegate {
objectName: "CollectibleSelector_ItemDelegate_" + groupId
width: holdingItemSelector.comboBoxControl.popup.width
highlighted: !!holdingItemSelector.selectedItem && uid === holdingItemSelector.selectedItem.uid
onItemSelected: {
if (isGroup) {
d.currentBrowsingGroupName = groupName

View File

@ -4,6 +4,8 @@ import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Controls.Validators 0.1
import "../controls"
@ -14,7 +16,7 @@ ColumnLayout {
id: root
readonly property alias input: topAmountToSendInput
readonly property bool inputNumberValid: !!input.text && !isNaN(d.parsedInput)
readonly property bool inputNumberValid: !!input.text && !isNaN(d.parsedInput) && input.valid
readonly property int minSendCryptoDecimals:
!inputIsFiat ? LocaleUtils.fractionalPartLength(d.inputNumber) : 0
@ -36,6 +38,10 @@ ColumnLayout {
property bool interactive: false
property bool inputIsFiat: false
property string caption: isBridgeTx ? qsTr("Amount to bridge") : qsTr("Amount to send")
property bool fiatInputInteractive: true
// Crypto value to send expressed in base units (like wei for ETH),
// as a string representing integer decimal
readonly property alias cryptoValueToSend: d.cryptoValueRawToSend
@ -45,6 +51,8 @@ ColumnLayout {
property var formatCurrencyAmount:
(amount, symbol, options = null, locale = null) => {}
property bool loading
signal reCalculateSuggestedRoute()
QtObject {
@ -88,11 +96,11 @@ ColumnLayout {
}
readonly property string zeroString:
LocaleUtils.numberToLocaleString(0, 2, LocaleUtils.userInputLocale)
LocaleUtils.numberToLocaleString(0, 2, topAmountToSendInput.locale)
readonly property double parsedInput:
LocaleUtils.numberFromLocaleString(topAmountToSendInput.text,
LocaleUtils.userInputLocale)
topAmountToSendInput.locale)
readonly property double inputNumber:
root.inputNumberValid ? d.parsedInput : 0
@ -108,10 +116,7 @@ ColumnLayout {
}
StatusBaseText {
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
text: root.isBridgeTx ? qsTr("Amount to bridge")
: qsTr("Amount to send")
text: root.caption
font.pixelSize: 13
lineHeight: 18
lineHeightMode: Text.FixedHeight
@ -119,17 +124,16 @@ ColumnLayout {
}
RowLayout {
Layout.fillWidth: true
id: topItem
property double topAmountToSend: !inputIsFiat ? d.cryptoValueToSend
: d.fiatValueToSend
property string topAmountSymbol: !inputIsFiat ? d.selectedSymbol
: root.currentCurrency
Layout.alignment: Qt.AlignLeft
AmountInputWithCursor {
id: topAmountToSendInput
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
Layout.fillWidth: true
Layout.maximumWidth: 250
Layout.preferredWidth: !!text ? input.edit.paintedWidth + 2
: textMetrics.advanceWidth
@ -138,31 +142,29 @@ ColumnLayout {
: Theme.palette.dangerColor1
input.edit.readOnly: !root.interactive
validationMode: StatusInput.ValidationMode.Always
validators: [
StatusFloatValidator {
id: floatValidator
bottom: 0
errorMessage: ""
locale: topAmountToSendInput.locale
},
StatusValidator {
errorMessage: ""
validate: (text) => {
const num = parseFloat(text)
var num = 0
try {
num = Number.fromLocaleString(topAmountToSendInput.locale, text)
} catch (e) {
console.warn(e, "(Error parsing number from text: %1)".arg(text))
return false
}
if (isNaN(num))
return true
return num <= root.maxInputBalance
}
return num > 0 && num <= root.maxInputBalance
}
}
]
TextMetrics {
id: textMetrics
text: topAmountToSendInput.placeholderText
font: topAmountToSendInput.input.placeholder.font
font: topAmountToSendInput.placeholderFont
}
Keys.onReleased: {
@ -172,33 +174,35 @@ ColumnLayout {
if (!isNaN(amount))
d.waitTimer.restart()
}
visible: !root.loading
}
LoadingComponent {
objectName: "topAmountToSendInputLoadingComponent"
Layout.preferredWidth: topAmountToSendInput.width
Layout.preferredHeight: topAmountToSendInput.height
visible: root.loading
}
}
Item {
StatusBaseText {
Layout.maximumWidth: parent.width
id: bottomItem
objectName: "bottomItemText"
property double bottomAmountToSend: inputIsFiat ? d.cryptoValueToSend
: d.fiatValueToSend
property string bottomAmountSymbol: inputIsFiat ? d.selectedSymbol
: currentCurrency
Layout.alignment: Qt.AlignLeft | Qt.AlignBottom
Layout.preferredWidth: txtBottom.width
Layout.preferredHeight: txtBottom.height
StatusBaseText {
id: txtBottom
anchors.top: parent.top
anchors.left: parent.left
text: root.formatCurrencyAmount(bottomItem.bottomAmountToSend,
bottomItem.bottomAmountSymbol)
font.pixelSize: 13
color: Theme.palette.directColor5
}
readonly property double bottomAmountToSend: inputIsFiat ? d.cryptoValueToSend
: d.fiatValueToSend
readonly property string bottomAmountSymbol: inputIsFiat ? d.selectedSymbol
: root.currentCurrency
elide: Text.ElideMiddle
text: root.formatCurrencyAmount(bottomAmountToSend, bottomAmountSymbol)
font.pixelSize: 13
color: Theme.palette.directColor5
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
cursorShape: enabled ? Qt.PointingHandCursor : undefined
enabled: root.fiatInputInteractive && !!root.selectedHolding
onClicked: {
topAmountToSendInput.validate()
@ -207,12 +211,19 @@ ColumnLayout {
bottomItem.bottomAmountToSend,
bottomItem.bottomAmountSymbol,
{ noSymbol: true, rawAmount: true },
LocaleUtils.userInputLocale)
topAmountToSendInput.locale)
}
inputIsFiat = !inputIsFiat
root.inputIsFiat = !root.inputIsFiat
d.waitTimer.restart()
}
}
visible: !root.loading
}
LoadingComponent {
objectName: "bottomItemTextLoadingComponent"
Layout.preferredWidth: bottomItem.width
Layout.preferredHeight: bottomItem.height
visible: root.loading
}
}

View File

@ -20,7 +20,6 @@ import "../controls"
Item {
id: root
property string selectedSenderAccount
property var assets: null
property var collectibles: null
property var networksModel
@ -221,7 +220,6 @@ Item {
TokenBalancePerChainDelegate {
width: tokenList.width
selectedSenderAccount: root.selectedSenderAccount
balancesModel: LeftJoinModel {
leftModel: !!model & !!model.balances ? model.balances : null
rightModel: root.networksModel
@ -230,7 +228,7 @@ Item {
onTokenSelected: function (selectedToken) {
root.tokenSelected(selectedToken.symbol, Constants.TokenType.ERC20)
}
onTokenHovered: root.tokenHovered(symbol, Constants.TokenType.ERC20, hovered)
onTokenHovered: root.tokenHovered(selectedToken.symbol, Constants.TokenType.ERC20, hovered)
formatCurrentCurrencyAmount: function(balance){
return root.formatCurrentCurrencyAmount(balance)
}
@ -253,13 +251,13 @@ Item {
id: collectiblesDelegate
CollectibleNestedDelegate {
width: tokenList.width
onItemHovered: root.tokenHovered(selectedItem.uid, tokenType, hovered)
onItemHovered: root.tokenHovered(selectedItem.uid, Constants.TokenType.ERC721, hovered)
onItemSelected: {
if (isGroup) {
d.currentBrowsingGroupName = groupName
root.collectibles.currentGroupId = groupId
} else {
root.tokenSelected(selectedItem.uid, tokenType)
root.tokenSelected(selectedItem.uid, Constants.TokenType.ERC721)
}
}
}

View File

@ -1,6 +1,7 @@
import QtQuick 2.15
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import utils 1.0
import AppLayouts.Profile.stores 1.0
@ -982,12 +983,18 @@ QtObject {
function formatCurrencyAmount(amount, symbol, options = null, locale = null) {
if (isNaN(amount)) {
return "N/A"
return qsTr("N/A")
}
var currencyAmount = getCurrencyAmount(amount, symbol)
return LocaleUtils.currencyAmountToLocaleString(currencyAmount, options, locale)
}
function formatCurrencyAmountFromBigInt(balance, symbol, decimals) {
let bigIntBalance = SQUtils.AmountsArithmetic.fromString(balance)
let decimalBalance = SQUtils.AmountsArithmetic.toNumber(bigIntBalance, decimals)
return formatCurrencyAmount(decimalBalance, symbol)
}
function getFiatValue(cryptoAmount, cryptoSymbol) {
var amount = _profileSectionModuleInst.ensUsernamesModule.getFiatValue(cryptoAmount, cryptoSymbol)
return parseFloat(amount)

View File

@ -14,7 +14,7 @@ import AppLayouts.Wallet.stores 1.0
QtObject {
id: root
property CurrenciesStore currencyStore: CurrenciesStore {}
property CurrenciesStore currencyStore
property WalletAssetsStore walletAssetStore
property TokensStore tokensStore
@ -276,10 +276,12 @@ QtObject {
submodelRoleName: "balances"
delegateModel: SortFilterProxyModel {
sourceModel: submodel
filters: FastExpressionFilter {
expression: root.selectedSenderAccount.address === model.account
expectedRoles: ["account"]
}
filters: [
ValueFilter {
roleName: "account"
value: root.selectedSenderAccount.address
}
]
}
}
@ -302,7 +304,7 @@ QtObject {
},
FastExpressionRole {
name: "currentBalance"
expression: __getTotalBalance(model.balances, model.decimals, root.selectedSenderAccount)
expression: __getTotalBalance(model.balances, model.decimals)
expectedRoles: ["balances", "decimals"]
},
FastExpressionRole {
@ -324,7 +326,7 @@ QtObject {
name.toUpperCase().startsWith(searchString.toUpperCase()) || __searchAddressInList(addressPerChain, searchString)
)
}
expression: search(symbol, name, addressPerChain, assetSearchString)
expression: search(symbol, name, addressPerChain, root.assetSearchString)
expectedRoles: ["symbol", "name", "addressPerChain"]
},
ValueFilter {