import QtQml 2.15 import QtQuick 2.15 import QtQuick.Layouts 1.15 import SortFilterProxyModel 0.2 import StatusQ 0.1 import StatusQ.Controls 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Components 0.1 import utils 1.0 import shared.controls 1.0 import shared.popups 1.0 import shared.popups.send 1.0 import "../controls" Item { id: root property var assetsModel property var collectiblesModel property var networksModel property bool onlyAssets: true property string searchText implicitWidth: holdingItemSelector.implicitWidth implicitHeight: holdingItemSelector.implicitHeight property var formatCurrentCurrencyAmount: function(balance){} property var formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){} signal itemHovered(string holdingId, var holdingType) signal itemSelected(string holdingId, var holdingType) 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 d.currentHoldingType = holdingType holdingItemSelector.selectedItem = item } function setHoveredItem(item, holdingType) { d.browsingHoldingType = holdingType holdingItemSelector.hoveredItem = null d.currentHoldingType = holdingType holdingItemSelector.hoveredItem = item } QtObject { id: d // Internal management properties and signals: readonly property var holdingTypes: onlyAssets ? [Constants.TokenType.ERC20] : [Constants.TokenType.ERC20, Constants.TokenType.ERC721] readonly property var tabsModel: onlyAssets ? [qsTr("Assets")] : [qsTr("Assets"), qsTr("Collectibles")] readonly property var updateSearchText: Backpressure.debounce(root, 500, function(inputText) { root.searchText = inputText }) function isAsset(type) { return type === Constants.TokenType.ERC20 } function isCommunityItem(type) { return type === Constants.CollectiblesNestedItemType.CommunityCollectible || type === Constants.CollectiblesNestedItemType.Community } function isGroupItem(type) { return type === Constants.CollectiblesNestedItemType.Collection || type === Constants.CollectiblesNestedItemType.Community } property int browsingHoldingType: Constants.TokenType.ERC20 readonly property bool isCurrentBrowsingTypeAsset: isAsset(browsingHoldingType) readonly property bool isBrowsingGroup: !isCurrentBrowsingTypeAsset && !!root.collectiblesModel && root.collectiblesModel.currentGroupId !== "" property string currentBrowsingGroupName property var currentHoldingType: Constants.TokenType.Unknown readonly property string uppercaseSearchText: searchText.toUpperCase() property var assetTextFn: function (asset) { return !!asset && asset.symbol ? asset.symbol : "" } property var assetIconSourceFn: function (asset) { if (!asset) { return "" } else if (asset.image) { // Community assets have a dedicated image streamed from status-go return asset.image } return Constants.tokenIcon(asset.symbol) } property var collectibleTextFn: function (item) { if (!!item) { return !!item.groupName ? item.groupName + ": " + item.name : item.name } return "" } property var collectibleIconSourceFn: function (item) { return !!item && item.iconUrl ? item.iconUrl : "" } readonly property RolesRenamingModel renamedAllNetworksModel: RolesRenamingModel { sourceModel: root.networksModel mapping: RoleRename { from: "iconUrl" to: "networkIconUrl" } } readonly property LeftJoinModel collectibleNetworksJointModel: LeftJoinModel { leftModel: root.collectiblesModel rightModel: d.renamedAllNetworksModel joinRole: "chainId" } property var collectibleComboBoxModel: SortFilterProxyModel { sourceModel: d.collectibleNetworksJointModel proxyRoles: [ FastExpressionRole { name: "isCommunityAsset" expression: d.isCommunityItem(model.itemType) expectedRoles: ["itemType"] }, FastExpressionRole { name: "isGroup" expression: d.isGroupItem(model.itemType) expectedRoles: ["itemType"] } ] filters: [ ExpressionFilter { expression: { return d.uppercaseSearchText === "" || name.toUpperCase().startsWith(d.uppercaseSearchText) } } ] sorters: [ RoleSorter { roleName: "isCommunityAsset" sortOrder: Qt.DescendingOrder }, RoleSorter { roleName: "isGroup" sortOrder: Qt.DescendingOrder } ] } // By design values: readonly property int padding: 16 readonly property int headerTopMargin: 5 readonly property int tabBarTopMargin: 20 readonly property int tabBarHeight: 35 readonly property int bottomInset: 20 readonly property int assetContentIconSize: 21 readonly property int collectibleContentIconSize: 28 readonly property int assetContentTextSize: 28 readonly property int collectibleContentTextSize: 15 } HoldingItemSelector { id: holdingItemSelector width: parent.width height: parent.height defaultIconSource: Style.png("tokens/DEFAULT-TOKEN@3x") placeholderText: d.isCurrentBrowsingTypeAsset ? qsTr("Select token") : qsTr("Select collectible") property bool hasCommunityTokens: false comboBoxDelegate: Item { property var itemModel: model // read 'model' from the delegate's context width: loader.width height: loader.height Loader { id: loader // inject model properties to the loaded item's context // common property var model: itemModel property var chainId: model.chainId property var name: model.name property var tokenType: model.tokenType // asset property var symbol: model.symbol property var totalBalance: model.totalBalance property var marketDetails: model.marketDetails property var decimals: model.decimals property var balances: model.balances // collectible property var uid: model.uid property var iconUrl: model.iconUrl property var networkIconUrl: model.networkIconUrl property var groupId: model.groupId property var groupName: model.groupName property var isGroup: model.isGroup property var count: model.count sourceComponent: d.isCurrentBrowsingTypeAsset ? assetComboBoxDelegate : collectibleComboBoxDelegate } } comboBoxModel: d.isCurrentBrowsingTypeAsset ? root.assetsModel : d.collectibleComboBoxModel comboBoxPopupHeader: headerComponent itemTextFn: d.isCurrentBrowsingTypeAsset ? d.assetTextFn : d.collectibleTextFn itemIconSourceFn: d.isCurrentBrowsingTypeAsset ? d.assetIconSourceFn : d.collectibleIconSourceFn onComboBoxModelChanged: updateHasCommunityTokens() function updateHasCommunityTokens() { hasCommunityTokens = Helpers.modelHasCommunityTokens(comboBoxModel, d.isCurrentBrowsingTypeAsset) } 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 required property bool section 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 { id: communityInfoPopupCmp CommunityAssetsInfoPopup {} } Component { id: headerComponent ColumnLayout { function focusSearch() { searchInput.input.forceActiveFocus() } function clear() { searchInput.input.edit.clear() } width: holdingItemSelector.comboBoxControl.popup.width Layout.topMargin: d.headerTopMargin spacing: -1 // Used to overlap rectangles from row components StatusTabBar { id: tabBar visible: !root.onlyAssets Layout.preferredHeight: d.tabBarHeight Layout.fillWidth: true Layout.leftMargin: d.padding Layout.rightMargin: d.padding Layout.topMargin: d.tabBarTopMargin Layout.bottomMargin: 6 currentIndex: d.holdingTypes.indexOf(d.browsingHoldingType) onCurrentIndexChanged: { if (currentIndex >= 0) { d.browsingHoldingType = d.holdingTypes[currentIndex] } } Repeater { id: tabLabelsRepeater model: d.tabsModel StatusTabButton { text: modelData width: implicitWidth } } } CollectibleBackButtonWithInfo { Layout.fillWidth: true visible: d.isBrowsingGroup count: collectiblesModel ? collectiblesModel.count : 0 name: d.currentBrowsingGroupName onBackClicked: { if (!d.isCurrentBrowsingTypeAsset) { searchInput.reset() root.collectiblesModel.currentGroupId = "" } } } Rectangle { Layout.fillWidth: true Layout.preferredHeight: searchInput.input.implicitHeight color: "transparent" border.color: Theme.palette.baseColor2 border.width: 1 StatusInput { id: searchInput anchors.fill: parent input.showBackground: false placeholderText: root.searchPlaceholderText onTextChanged: Qt.callLater(d.updateSearchText, text) input.clearable: true input.implicitHeight: 56 input.rightComponent: StatusFlatRoundButton { icon.name: "search" type: StatusFlatRoundButton.Type.Secondary enabled: false } } } } } Component { id: assetComboBoxDelegate TokenBalancePerChainDelegate { objectName: "AssetSelector_ItemDelegate_" + symbol width: holdingItemSelector.comboBoxControl.popup.width highlighted: !!holdingItemSelector.selectedItem && symbol === holdingItemSelector.selectedItem.symbol balancesModel: LeftJoinModel { leftModel: balances rightModel: root.networksModel joinRole: "chainId" } onTokenSelected: function (selectedToken) { holdingItemSelector.selectedItem = selectedToken d.currentHoldingType = Constants.TokenType.ERC20 root.itemSelected(selectedToken.symbol, Constants.TokenType.ERC20) holdingItemSelector.comboBoxControl.popup.close() } formatCurrentCurrencyAmount: function(balance){ return root.formatCurrentCurrencyAmount(balance) } formatCurrencyAmountFromBigInt: function(balance, symbol, decimals){ return root.formatCurrencyAmountFromBigInt(balance, symbol, decimals) } } } Component { id: collectibleComboBoxDelegate CollectibleNestedDelegate { objectName: "CollectibleSelector_ItemDelegate_" + groupId width: holdingItemSelector.comboBoxControl.popup.width highlighted: !!holdingItemSelector.selectedItem && uid === holdingItemSelector.selectedItem.uid onItemSelected: { if (isGroup) { d.currentBrowsingGroupName = groupName root.collectiblesModel.currentGroupId = groupId } else { holdingItemSelector.selectedItem = selectedItem d.currentHoldingType = tokenType root.itemSelected(selectedItem.uid, tokenType) holdingItemSelector.comboBoxControl.popup.close() } } } } }