status-desktop/ui/imports/shared/views/AssetsView.qml

337 lines
12 KiB
QML

import QtQuick 2.15
import QtQml 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups.Dialog 0.1
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStores
import shared.controls 1.0
import shared.popups 1.0
import shared.stores 1.0
import utils 1.0
import SortFilterProxyModel 0.2
Control {
id: root
/**
Expected model structure:
key [string] - unique identifier of a token, e.g "0x3234235"
symbol [string] - token's symbol e.g. "ETH" or "SNT"
name [string] - token's name e.g. "Ether" or "Dai"
icon [url] - token's icon url
balance [double] - tokens balance is the commonly used unit, e.g. 1.2 for 1.2 ETH, used
for sorting and computing market value
balanceText [string] - formatted and localized balance. This is not done internally because
it may depend on many external factors
error [string] - error message related to balance
marketDetailsAvailable [bool] - specifies if market datails are available for given token
marketDetailsLoading [bool] - specifies if market datails are available for given token
marketPrice [double] - specifies market price in currently used currency
marketChangePct24hour [double] - percentage price change in last 24 hours, e.g. 0.5 for 0.5% of price change
communityId [string] - for community assets, unique identifier of a community, e.g. "0x6734235"
communityName [string] - for community assets, name of a community e.g. "Crypto Kitties"
communityIcon [url] - for community assets, community's icon url
position [int] - if custom order available, display position defined by the user via token management
canBeHidden [bool] - specifies if given token can be hidden (e.g. ETH should be always visible)
**/
property var model
// enables global loading state useful when real data are not yet available
property bool loading
// shows/hides list sorter
property bool sorterVisible
// allows/disables choosing custom sort order from a sorter
property bool customOrderAvailable
// switches configuring right click menu
property bool sendEnabled: true
property bool communitySendEnabled: false
property bool swapEnabled: true
property bool swapVisible: true
property bool communitySwapVisible: false
property string balanceError
// global market data error, presented for all tokens expecting market data
property string marketDataError
// formatting function for fiat currency values
property var formatFiat: balance => `${balance.toLocaleString(Qt.locale())} XYZ`
signal sendRequested(string key)
signal receiveRequested(string key)
signal swapRequested(string key)
signal assetClicked(string key)
signal communityClicked(string communityKey)
signal hideRequested(string key)
signal hideCommunityAssetsRequested(string communityKey)
signal manageTokensRequested
function setSortOrder(order) {
d.sortOrder = order
}
function getSortOrder() {
return d.sortOrder
}
function getSortValue() {
return d.sortValue
}
function sortByValue(value) {
d.sortValue = value
}
QtObject {
id: d
readonly property int loadingItemsCount: 25
property int sortOrder: Qt.DescendingOrder
property int sortValue: -1
}
SortFilterProxyModel {
id: sfpm
sourceModel: root.model ?? null
proxyRoles: [
// helper role for rendering section delegate
FastExpressionRole {
name: "isCommunity"
expression: !!communityId ? "community" : ""
expectedRoles: ["communityId"]
},
FastExpressionRole {
name: "marketBalance"
expression: balance * marketPrice
expectedRoles: ["balance", "marketPrice"]
},
FastExpressionRole {
name: "change1DayFiat"
expression: marketBalance * (1 - (1 / (marketChangePct24hour / 100 + 1)))
expectedRoles: ["marketBalance", "marketChangePct24hour"]
}
]
sorters: [
RoleSorter {
roleName: "isCommunity"
},
RoleSorter {
roleName: sortOrderComboBox.currentSortRoleName
sortOrder: sortOrderComboBox.currentSortOrder
}
]
}
contentItem: ColumnLayout {
ColumnLayout {
Layout.fillHeight: false
Layout.preferredHeight: root.sorterVisible ? implicitHeight : 0
opacity: root.sorterVisible ? 1 : 0
spacing: 20
visible: opacity > 0
Behavior on Layout.preferredHeight {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
Behavior on opacity {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
StatusDialogDivider { Layout.fillWidth: true }
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: false
spacing: Style.current.halfPadding
StatusBaseText {
color: Theme.palette.baseColor1
font.pixelSize: Style.current.additionalTextSize
text: qsTr("Sort by:")
}
SortOrderComboBox {
id: sortOrderComboBox
objectName: "cmbTokenOrder"
hasCustomOrderDefined: root.customOrderAvailable
Binding on currentIndex {
value: {
sortOrderComboBox.count
let id = sortOrderComboBox.indexOfValue(d.sortValue)
if (id === -1)
id = sortOrderComboBox.indexOfValue(SortOrderComboBox.TokenOrderAlpha)
return id
}
when: sortOrderComboBox.count > 0
}
onCurrentValueChanged: d.sortValue = sortOrderComboBox.currentValue
Binding on currentSortOrder {
value: d.sortOrder
}
onCurrentSortOrderChanged: d.sortOrder = sortOrderComboBox.currentSortOrder
model: [
{ value: SortOrderComboBox.TokenOrderCurrencyBalance,
text: qsTr("Asset balance value"), icon: "", sortRoleName: "marketBalance" },
{ value: SortOrderComboBox.TokenOrderBalance,
text: qsTr("Asset balance"), icon: "", sortRoleName: "balance" },
{ value: SortOrderComboBox.TokenOrderCurrencyPrice,
text: qsTr("Asset value"), icon: "", sortRoleName: "marketPrice" },
{ value: SortOrderComboBox.TokenOrder1DChange,
text: qsTr("1d change: balance value"), icon: "", sortRoleName: "change1DayFiat" },
{ value: SortOrderComboBox.TokenOrderAlpha,
text: qsTr("Asset name"), icon: "", sortRoleName: "name" },
{ value: SortOrderComboBox.TokenOrderCustom,
text: qsTr("Custom order"), icon: "", sortRoleName: "position" },
{ value: SortOrderComboBox.TokenOrderNone,
text: "---", icon: "", sortRoleName: "" }, // separator
{ value: SortOrderComboBox.TokenOrderCreateCustom,
text: hasCustomOrderDefined ? qsTr("Edit custom order →") : qsTr("Create custom order →"),
icon: "", sortRoleName: "" }
]
onCreateOrEditRequested: {
root.manageTokensRequested()
}
}
}
StatusDialogDivider { Layout.fillWidth: true }
}
DelegateModel {
id: regularModel
model: sfpm
delegate: TokenDelegate {
objectName: `AssetView_TokenListItem_${model.symbol}`
width: ListView.view.width
name: model.name
icon: model.icon
balance: model.balanceText
marketBalance: root.formatFiat(model.marketBalance)
marketDetailsAvailable: model.marketDetailsAvailable
marketDetailsLoading: model.marketDetailsLoading
marketCurrencyPrice: root.formatFiat(model.change1DayFiat)
marketChangePct24hour: model.marketChangePct24hour
communityId: model.communityId
communityName: model.communityName ?? ""
communityIcon: model.communityIcon ?? ""
errorTooltipText_1: model.error
errorTooltipText_2: root.marketDataError
errorMode: !!root.balanceError
errorIcon.tooltip.text: root.balanceError
onClicked: {
if (mouse.button === Qt.LeftButton)
root.assetClicked(model.key)
else if (mouse.button === Qt.RightButton)
tokenContextMenu.createObject(this, { model }).popup(mouse)
}
onCommunityClicked: root.communityClicked(model.communityId)
}
}
DelegateModel {
id: loadingModel
model: d.loadingItemsCount
delegate: LoadingTokenDelegate {
objectName: `AssetView_LoadingTokenDelegate_${model.index}`
width: ListView.view.width
}
}
StatusListView {
id: listView
objectName: "assetViewStatusListView"
Layout.fillWidth: true
Layout.fillHeight: true
model: root.loading ? loadingModel : regularModel
section {
property: "isCommunity"
delegate: AssetsSectionDelegate {
width: parent.width
text: qsTr("Community minted")
onInfoButtonClicked: communityInfoPopup.createObject(this).open()
}
}
}
}
Component {
id: tokenContextMenu
AssetContextMenu {
required property var model
readonly property string key: model.key
readonly property string communityKey: model.communityId
readonly property bool isCommunity: !!model.isCommunity
onClosed: destroy()
sendEnabled: root.sendEnabled
&& (!isCommunity || root.communitySendEnabled)
swapEnabled: root.swapEnabled
swapVisible: root.swapVisible && (!isCommunity || root.communitySwapVisible)
hideVisible: model.canBeHidden
communityHideVisible: isCommunity
onSendRequested: root.sendRequested(key)
onReceiveRequested: root.receiveRequested(key)
onSwapRequested: root.swapRequested(key)
onHideRequested: root.hideRequested(key)
onCommunityHideRequested: root.hideCommunityAssetsRequested(communityKey)
onManageTokensRequested: root.manageTokensRequested()
}
}
Component {
id: communityInfoPopup
CommunityAssetsInfoPopup {
destroyOnClose: true
}
}
}