import QtQuick 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 shared.controls 1.0 import shared.popups 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 swapEnabled: true property bool swapVisible: true 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 hideCommunityAssets(string communityKey) signal manageTokensRequested QtObject { id: d readonly property int loadingItemsCount: 25 } 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 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 onClosed: destroy() sendEnabled: root.sendEnabled swapEnabled: root.swapEnabled swapVisible: root.swapVisible hideVisible: model.canBeHidden communityHideVisible: !!model.isCommunity onSendRequested: root.sendRequested(key) onReceiveRequested: root.receiveRequested(key) onSwapRequested: root.swapRequested(key) onHideRequested: confirmHideAssetPopup.createObject(parent, { model }).open() onCommunityHideRequested: confirmHideCommunityAssetsPopup.createObject(parent, { model }).open() onManageTokensRequested: root.manageTokensRequested() } } Component { id: communityInfoPopup CommunityAssetsInfoPopup { destroyOnClose: true } } Component { id: confirmHideAssetPopup ConfirmHideAssetPopup { destroyOnClose: true required property var model symbol: model.symbol name: model.name icon: model.icon onConfirmButtonClicked: { root.hideRequested(model.key) close() } } } Component { id: confirmHideCommunityAssetsPopup ConfirmHideCommunityAssetsPopup { required property var model name: model.communityName icon: model.communityIcon onConfirmButtonClicked: { root.hideCommunityAssets(model.communityId) close(); } } } }