feat(Wallet): New tokens selector and related adaptor

Closes: #15121
This commit is contained in:
Michał Cieślak 2024-07-03 13:47:05 +02:00 committed by Michał
parent 1d728a5241
commit 4a6257282e
15 changed files with 1793 additions and 18 deletions

View File

@ -0,0 +1,391 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Models 0.1
import AppLayouts.Wallet.controls 1.0
import AppLayouts.Wallet.adaptors 1.0
import utils 1.0
import Storybook 1.0
import SortFilterProxyModel 0.2
Pane {
id: root
ListModel {
id: listModel
readonly property var data: [
// collection 2
{
tokenId: "id_3",
name: "Multi-sequencer Test NFT 1",
contractAddress: "contract_2",
collectionName: "Multi-sequencer Test NFT",
collectionUid: "collection_2",
ownership: [
{
accountAddress: "account_1",
balance: 1,
txTimestamp: 1714059810
}
],
imageUrl: Constants.tokenIcon("ETH", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl("")
},
{
tokenId: "id_4",
name: "Multi-sequencer Test NFT 2",
contractAddress: "contract_2",
collectionName: "Multi-sequencer Test NFT",
collectionUid: "collection_2",
ownership: [
{
accountAddress: "account_1",
balance: 1,
txTimestamp: 1714059811
}
],
imageUrl: Constants.tokenIcon("ETH", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl("")
},
{
tokenId: "id_5",
name: "Multi-sequencer Test NFT 3",
contractAddress: "contract_2",
collectionName: "Multi-sequencer Test NFT",
collectionUid: "collection_2",
ownership: [
{
accountAddress: "account_1",
balance: 1,
txTimestamp: 1714059899
}
],
imageUrl: Constants.tokenIcon("ETH", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl("")
},
// collection 1
{
tokenId: "id_1",
name: "Genesis",
contractAddress: "contract_1",
collectionName: "ERC-1155 Faucet",
collectionUid: "collection_1",
ownership: [
{
accountAddress: "account_1",
balance: 23,
txTimestamp: 1714059862
},
{
accountAddress: "account_2",
balance: 29,
txTimestamp: 1714054862
}
],
imageUrl: Constants.tokenIcon("DAI", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl("")
},
{
tokenId: "id_2",
name: "QAERC1155",
contractAddress: "contract_1",
collectionName: "ERC-1155 Faucet",
collectionUid: "collection_1",
ownership: [
{
accountAddress: "account_1",
balance: 500,
txTimestamp: 1714059864
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "",
communityName: "",
communityImage: Qt.resolvedUrl("")
},
// collection 3, community token
{
tokenId: "id_6",
name: "My Token",
contractAddress: "contract_3",
collectionName: "My Token",
collectionUid: "collection_3",
ownership: [
{
accountAddress: "account_1",
balance: 1,
txTimestamp: 1714059899
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_1",
communityName: "My community",
communityImage: Constants.tokenIcon("KIN", false)
},
{
tokenId: "id_7",
name: "My Token",
contractAddress: "contract_3",
collectionName: "My Token",
collectionUid: "collection_3",
ownership: [
{
accountAddress: "account_1",
balance: 1,
txTimestamp: 1714059899
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_1",
communityName: "My community",
communityImage: Constants.tokenIcon("KIN", false)
},
{
tokenId: "id_8",
name: "My Token",
contractAddress: "contract_3",
collectionName: "My Token",
collectionUid: "collection_3",
ownership: [
{
accountAddress: "account_2",
balance: 1,
txTimestamp: 1714059999
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_1",
communityName: "My community",
communityImage: Constants.tokenIcon("KIN", false)
},
{
tokenId: "id_9",
name: "My Other Token",
contractAddress: "contract_4",
collectionName: "My Other Token",
collectionUid: "collection_4",
ownership: [
{
accountAddress: "account_1",
balance: 1,
txTimestamp: 1714059991
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_1",
communityName: "My community",
communityImage: Constants.tokenIcon("KIN", false)
},
{
tokenId: "id_10",
name: "My Community 2 Token",
contractAddress: "contract_5",
collectionName: "My Community 2 Token",
collectionUid: "collection_5",
ownership: [
{
accountAddress: "account_1",
balance: 1,
txTimestamp: 1714059777
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_2",
communityName: "My community 2",
communityImage: Constants.tokenIcon("ICOS", false)
},
{
tokenId: "id_11",
name: "My Community 2 Token",
contractAddress: "contract_5",
collectionName: "My Community 2 Token",
collectionUid: "collection_5",
ownership: [
{
accountAddress: "account_1",
balance: 1,
txTimestamp: 1714059778
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_2",
communityName: "My community 2",
communityImage: Constants.tokenIcon("ICOS", false)
},
{
tokenId: "id_11",
name: "My Community 2 Token",
contractAddress: "contract_5",
collectionName: "My Community 2 Token",
collectionUid: "collection_5",
ownership: [
{
accountAddress: "account_2",
balance: 1,
txTimestamp: 1714059779
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_2",
communityName: "My community 2",
communityImage: Constants.tokenIcon("ICOS", false)
},
{
tokenId: "id_12",
name: "My Community 2 Token",
contractAddress: "contract_5",
collectionName: "My Community 2 Token",
collectionUid: "collection_5",
ownership: [
{
accountAddress: "account_3",
balance: 1,
txTimestamp: 1714059779
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_2",
communityName: "My community 2",
communityImage: Constants.tokenIcon("ICOS", false)
},
{
tokenId: "id_13",
name: "My Community 2 Token",
contractAddress: "contract_5",
collectionName: "My Community 2 Token",
collectionUid: "collection_5",
ownership: [
{
accountAddress: "account_3",
balance: 1,
txTimestamp: 1714059788
}
],
imageUrl: Constants.tokenIcon("ZRX", false),
mediaUrl: Qt.resolvedUrl(""),
communityId: "community_2",
communityName: "My community 2",
communityImage: Constants.tokenIcon("ICOS", false)
}
]
Component.onCompleted: {
append(data)
const accounts = new Set()
data.forEach(e => e.ownership.forEach(
e => { accounts.add(e.accountAddress) }))
accountsSelector.model = [...accounts.values()]
}
}
CollectiblesSelectionAdaptor {
id: adaptor
collectiblesModel: listModel
accountKey: accountsSelector.selection
}
ColumnLayout {
anchors.fill: parent
TokenSelectorNew {
collectiblesModel: adaptor.model
}
RowLayout {
Label { text: "Accounts:" }
RadioButtonFlowSelector {
id: accountsSelector
Layout.fillWidth: true
}
}
RowLayout {
GenericListView {
label: "Input model"
model: listModel
Layout.fillWidth: true
Layout.fillHeight: true
skipEmptyRoles: true
}
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.vertical: ScrollBar {}
clip: true
spacing: 5
model: adaptor.model
delegate: ColumnLayout {
width: ListView.view.width
readonly property var submodel: model.subitems
readonly property int submodelCount:
model.subitems.ModelCount.count
Label {
text: (model.communityId ? "Community" : "Collection")
+ `: ${model.groupName} (count: ${submodelCount})`
}
Repeater {
model: submodel
Label {
text: "\t" + model.name + " (balance: " + model.balance + "), key: " + model.key
}
}
}
section.property: "type"
section.delegate: Label {
text: section
font.underline: true
font.bold: true
bottomPadding: 10
}
}
}
}
}
// category: Adaptors

View File

@ -0,0 +1,110 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import AppLayouts.Wallet.views 1.0
import StatusQ.Core.Theme 0.1
import utils 1.0
import Qt.labs.settings 1.0
SplitView {
id: root
orientation: Qt.Vertical
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
Rectangle {
color: Theme.palette.statusListItem.backgroundColor
border.color: Theme.palette.primaryColor1
border.width: 1
anchors.fill: delegate
anchors.margins: -30
}
TokenSelectorCollectibleDelegate {
id: delegate
implicitWidth: 330
anchors.centerIn: parent
name: nameTextField.text
balance: balanceSpinBox.value ? balanceSpinBox.value : ""
image: Constants.tokenIcon("ETH")
goDeeperIconVisible: goDeeperSwitch.checked
interactive: interactiveSwitch.checked
highlighted: highlightedSwitch.checked
}
}
Pane {
SplitView.minimumHeight: 250
SplitView.preferredHeight: 250
RowLayout {
anchors.fill: parent
ColumnLayout {
RowLayout {
Label {
text: "name:"
}
TextField {
id: nameTextField
text: "Crypto Kitties"
}
}
RowLayout {
Label {
text: "balance:"
}
SpinBox {
id: balanceSpinBox
value: 12
from: 0
to: 20
}
}
Switch {
id: interactiveSwitch
text: "Interactive"
checked: true
}
Switch {
id: highlightedSwitch
text: "Highlighted"
checked: false
}
Switch {
id: goDeeperSwitch
text: "Go deeper icon visible"
checked: false
}
Item { Layout.fillHeight: true }
}
}
}
Settings {
property alias interactiveSwitchChecked: interactiveSwitch.checked
property alias highlightedSwitchChecked: highlightedSwitch.checked
property alias goDeeperSwitchChecked: goDeeperSwitch.checked
}
}
// category: Delegates

View File

@ -0,0 +1,187 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import AppLayouts.Wallet.controls 1.0
import StatusQ.Core.Theme 0.1
import utils 1.0
Pane {
readonly property var assetsData: [
{
tokensKey: "key_1",
communityId: "",
name: "Status Test Token",
currencyBalanceAsString: "42,23 USD",
symbol: "STT",
iconSource: Constants.tokenIcon("STT"),
tokensKey: "STT",
balances: [
{
balanceAsString: "0,56",
iconUrl: "network/Network=Ethereum"
},
{
balanceAsString: "0,22",
iconUrl: "network/Network=Arbitrum"
},
{
balanceAsString: "0,12",
iconUrl: "network/Network=Optimism"
}
]
},
{
tokensKey: "key_2",
communityId: "",
name: "Ether",
currencyBalanceAsString: "4 276,86 USD",
symbol: "ETH",
iconSource: Constants.tokenIcon("ETH"),
tokensKey: "ETH",
balances: [
{
balanceAsString: "1,01",
iconUrl: "network/Network=Optimism"
},
{
balanceAsString: "0,47",
iconUrl: "network/Network=Arbitrum"
},
{
balanceAsString: "0,12",
iconUrl: "network/Network=Ethereum"
}
]
},
{
tokensKey: "key_2",
communityId: "",
name: "Dai Stablecoin",
currencyBalanceAsString: "45,92 USD",
symbol: "DAI",
iconSource: Constants.tokenIcon("DAI"),
tokensKey: "DAI",
balances: [
{
balanceAsString: "45,12",
iconUrl: "network/Network=Arbitrum"
},
{
balanceAsString: "0,56",
iconUrl: "network/Network=Optimism"
},
{
balanceAsString: "0,12",
iconUrl: "network/Network=Ethereum"
}
]
}
]
readonly property var collectiblesData: [
{
groupName: "My community",
icon: Constants.tokenIcon("BQX"),
type: "community",
subitems: [
{
key: "my_community_key_1",
name: "My token",
balance: 1,
icon: Constants.tokenIcon("CFI"),
}
]
},
{
groupName: "Crypto Kitties",
icon: Constants.tokenIcon("ENJ"),
type: "other",
subitems: [
{
key: "collection_1_key_1",
name: "Furbeard",
balance: 1,
icon: Constants.tokenIcon("FUEL"),
},
{
key: "collection_1_key_2",
name: "Magicat",
balance: 1,
icon: Constants.tokenIcon("ENJ"),
},
{
key: "collection_1_key_3",
name: "Happy Meow",
balance: 1,
icon: Constants.tokenIcon("FUN"),
}
]
},
{
groupName: "Super Rare",
icon: Constants.tokenIcon("CVC"),
type: "other",
subitems: [
{
key: "collection_2_key_1",
name: "Unicorn 1",
balance: 12,
icon: Constants.tokenIcon("CVC")
},
{
key: "collection_2_key_2",
name: "Unicorn 2",
balance: 1,
icon: Constants.tokenIcon("CVC")
}
]
},
{
groupName: "Unicorn",
icon: Constants.tokenIcon("ELF"),
type: "other",
subitems: [
{
key: "collection_3_key_1",
name: "Unicorn",
balance: 1,
icon: Constants.tokenIcon("ELF")
}
]
}
]
ListModel {
id: assetsModel
Component.onCompleted: append(assetsData)
}
ListModel {
id: collectiblesModel
Component.onCompleted: append(collectiblesData)
}
background: Rectangle {
color: Theme.palette.baseColor3
}
TokenSelectorNew {
id: panel
anchors.centerIn: parent
assetsModel: assetsModel
collectiblesModel: collectiblesModel
onCollectibleSelected: console.log("collectible selected:", key)
onCollectionSelected: console.log("collection selected:", key)
onAssetSelected: console.log("asset selected:", key)
}
}
// category: Controls

View File

@ -0,0 +1,236 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import AppLayouts.Wallet.panels 1.0
import utils 1.0
Pane {
readonly property var assetsData: [
{
tokensKey: "key_1",
communityId: "",
name: "Status Test Token",
currencyBalanceAsString: "42,23 USD",
symbol: "STT",
iconSource: Constants.tokenIcon("STT"),
tokensKey: "STT",
balances: [
{
balanceAsString: "0,56",
iconUrl: "network/Network=Ethereum"
},
{
balanceAsString: "0,22",
iconUrl: "network/Network=Arbitrum"
},
{
balanceAsString: "0,12",
iconUrl: "network/Network=Optimism"
}
]
},
{
tokensKey: "key_2",
communityId: "",
name: "Ether",
currencyBalanceAsString: "4 276,86 USD",
symbol: "ETH",
iconSource: Constants.tokenIcon("ETH"),
tokensKey: "ETH",
balances: [
{
balanceAsString: "1,01",
iconUrl: "network/Network=Optimism"
},
{
balanceAsString: "0,47",
iconUrl: "network/Network=Arbitrum"
},
{
balanceAsString: "0,12",
iconUrl: "network/Network=Ethereum"
}
]
},
{
tokensKey: "key_2",
communityId: "",
name: "Dai Stablecoin",
currencyBalanceAsString: "45,92 USD",
symbol: "DAI",
iconSource: Constants.tokenIcon("DAI"),
tokensKey: "DAI",
balances: [
{
balanceAsString: "45,12",
iconUrl: "network/Network=Arbitrum"
},
{
balanceAsString: "0,56",
iconUrl: "network/Network=Optimism"
},
{
balanceAsString: "0,12",
iconUrl: "network/Network=Ethereum"
}
]
}
]
readonly property var collectiblesData: [
{
groupName: "My community",
icon: Constants.tokenIcon("BQX"),
type: "community",
subitems: [
{
key: "my_community_key_1",
name: "My token",
balance: 1,
icon: Constants.tokenIcon("CFI"),
}
]
},
{
groupName: "Crypto Kitties",
icon: Constants.tokenIcon("ENJ"),
type: "other",
subitems: [
{
key: "collection_1_key_1",
name: "Furbeard",
balance: 1,
icon: Constants.tokenIcon("FUEL"),
},
{
key: "collection_1_key_2",
name: "Magicat",
balance: 1,
icon: Constants.tokenIcon("ENJ"),
},
{
key: "collection_1_key_3",
name: "Happy Meow",
balance: 1,
icon: Constants.tokenIcon("FUN"),
}
]
},
{
groupName: "Super Rare",
icon: Constants.tokenIcon("CVC"),
type: "other",
subitems: [
{
key: "collection_2_key_1",
name: "Unicorn 1",
balance: 12,
icon: Constants.tokenIcon("CVC")
},
{
key: "collection_2_key_2",
name: "Unicorn 2",
balance: 1,
icon: Constants.tokenIcon("CVC")
}
]
},
{
groupName: "Unicorn",
icon: Constants.tokenIcon("ELF"),
type: "other",
subitems: [
{
key: "collection_3_key_1",
name: "Unicorn",
balance: 1,
icon: Constants.tokenIcon("ELF")
}
]
}
]
ListModel {
id: assetsModel
Component.onCompleted: append(assetsData)
}
ListModel {
id: collectiblesModel
Component.onCompleted: append(collectiblesData)
}
Rectangle {
anchors.fill: panel
anchors.margins: -1
color: "transparent"
border.color: "lightgray"
}
TokenSelectorPanel {
id: panel
anchors.centerIn: parent
width: 350
assetsModel: assetsModelCheckBox.checked ? assetsModel : null
collectiblesModel: collectiblesModelCheckBox.checked
? collectiblesModel : null
onCollectibleSelected: {
highlightedKey = key
console.log("collectible selected:", key)
}
onCollectionSelected: {
highlightedKey = key
console.log("collection selected:", key)
}
onAssetSelected: {
highlightedKey = key
console.log("asset selected:", key)
}
}
RowLayout {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
Button {
text: "Select assets tab"
onClicked: panel.currentTab = TokenSelectorPanel.Tabs.Assets
}
Button {
text: "Select collectibles tab"
onClicked: panel.currentTab = TokenSelectorPanel.Tabs.Collectibles
}
CheckBox {
id: assetsModelCheckBox
checked: true
text: "Assets model assigned"
}
CheckBox {
id: collectiblesModelCheckBox
checked: true
text: "Collectibles model assigned"
}
}
}
// category: Controls

View File

@ -0,0 +1,185 @@
import QtQuick 2.15
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import SortFilterProxyModel 0.2
/**
Adaptor transforming input flat model of collectibles into grouped model,
grouped by communities and collections to form expected by components like
e.g. TokenSelector.
1. Ownership submodels filtered to have only entries for given `account`
2. Total balance calculated for remaining ownership entries in submodels
3. Balance exposed to the top level model
4. Grouping value exposed depending if token comes from community or not
(community collectibles are grouped by communtiyId, other by collectionId)
5. Entries with zero balance filtered out
6. Items are sorted by communityId and collectionId in order to group them correctly
7. Grouping by groupingValue
8. For community groups, group once again by collectionId
9. Expose groupName and type according of it's community group or collection
10. Expose sub-sub-groups count as a balance for sub-groups
**/
QObject {
id: root
/** Account key used for filtering **/
property string accountKey
/**
Expected model structure:
tokenId [string] - unique identifier of a collectible
collectionUid [string] - unique identifier of a collection
contractAddress [string] - collectible's contract address
name [string] - collectible's name e.g. "Magicat"
collectionName [string] - collection name e.g. "Crypto Kitties"
mediaUrl [url] - collectible's media url
imageUrl [url] - collectible's image url
communityId [string] - unique identifier of a community for community collectible or empty
ownership [model] - submodel of balances per chain/account
balance [int] - balance (always 1 for ERC-721)
accountAddress [string] - unique identifier of an account
**/
property var collectiblesModel
/**
Model structure:
groupName [string] - group name (from collection or community name)
icon [url] - from imageUrl or mediaUrl
type [string] - can be "community" or "other"
subitems [model] - submodel of collectibles/collections of the group
key [string] - key of collection (community type) or collectible (other type)
name [string] - name of the subitem (of collectible or collection)
balance [int] - balance of collection (in case of community collectibles)
or collectible (in case of ERC-1155)
icon [url] - icon of the subitem
**/
readonly property alias model: communityGroupsGrouppedByCollection
SortFilterProxyModel {
id: initiallyFilteredAndSorted
objectName: "collectiblesSelectionAdaptor_initiallyFilteredAndSorted"
sourceModel: ObjectProxyModel {
sourceModel: collectiblesModel ?? null
delegate: QObject {
readonly property int balance: balanceAggregator.value /* 3 */
readonly property string groupingValue: model.communityId /* 4 */
? model.communityId
: model.collectionUid
readonly property string key: model.tokenId
readonly property url icon:
model.imageUrl || model.mediaUrl || Qt.resolvedUrl("")
SortFilterProxyModel { /* 1 */
id: ownershipFiltered
sourceModel: model.ownership
filters: ValueFilter {
roleName: "accountAddress"
value: root.accountKey
}
}
SumAggregator { /* 2 */
id: balanceAggregator
model: ownershipFiltered
roleName: "balance"
}
}
expectedRoles: [
"ownership", "communityId", "collectionUid", "imageUrl",
"mediaUrl", "tokenId"
]
exposedRoles: ["balance", "groupingValue", "icon", "key"]
}
filters: RangeFilter { /* 5 */
roleName: "balance"
minimumValue: 1
}
sorters: [ /* 6 */
RoleSorter {
roleName: "communityId"
sortOrder: Qt.DescendingOrder
},
RoleSorter {
roleName: "collectionUid"
}
]
}
GroupingModel { /* 7 */
id: grouppedByCollectionOrCommunity
objectName: "collectiblesSelectionAdaptor_grouppedByCollectionOrCommunity"
sourceModel: initiallyFilteredAndSorted
groupingRoleName: "groupingValue"
submodelRoleName: "subitems"
}
ObjectProxyModel {
id: communityGroupsGrouppedByCollection
objectName: "collectiblesSelectionAdaptor_communityGroupsGrouppedByCollection"
sourceModel: grouppedByCollectionOrCommunity
delegate: QObject {
readonly property var subitems:
model.communityId ? collectionCountProxyLoader.item
: model.subitems
readonly property string type: /* 9 */
model.communityId ? "community" : "other"
readonly property string groupName:
model.communityName || model.collectionName
readonly property url icon:
model.communityId ? model.communityImage
: (model.icon || Qt.resolvedUrl(""))
Loader {
id: collectionCountProxyLoader
active: !!model.communityId
sourceComponent: ObjectProxyModel {
sourceModel: GroupingModel { /* 8 */
sourceModel: model.communityId ? model.subitems : null
groupingRoleName: "collectionUid"
submodelRoleName: "subitems"
}
delegate: QtObject { /* 10 */
readonly property int balance: model.subitems.ModelCount.count
readonly property string key: model.collectionUid
}
expectedRoles: ["subitems", "collectionUid"]
exposedRoles: ["balance", "key"]
}
}
}
expectedRoles: [
"subitems", "collectionName", "communityId",
"communityName", "communityImage", "icon"
]
exposedRoles: ["subitems", "type", "groupName", "icon"]
}
}

View File

@ -173,7 +173,7 @@ QObject {
RolesRenamingModel {
id: renamedTokensBySymbolModel
sourceModel: root.plainTokensBySymbolModel
sourceModel: root.plainTokensBySymbolModel || null
mapping: [
RoleRename {
from: "key"

View File

@ -1 +1,2 @@
CollectiblesSelectionAdaptor 1.0 CollectiblesSelectionAdaptor.qml
TokenSelectorViewAdaptor 1.0 TokenSelectorViewAdaptor.qml

View File

@ -0,0 +1,178 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Components 0.1
import StatusQ.Components.private 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import AppLayouts.Wallet.panels 1.0
import utils 1.0
Control {
id: root
/** Expected model structure: see TokenSelectorPanel::assetsModel **/
property alias assetsModel: tokenSelectorPanel.assetsModel
/** Expected model structure: see TokenSelectorPanel::collectiblesModel **/
property alias collectiblesModel: tokenSelectorPanel.collectiblesModel
signal assetSelected(string key)
signal collectionSelected(string key)
signal collectibleSelected(string key)
// Index of the current tab, indexes correspond to the
// TokensSelectorPanel.Tabs enum values.
property alias currentTab: tokenSelectorPanel.currentTab
function setCustom(name: string, icon: url, key: string) {
d.isTokenSelected = true
d.currentName = name
d.currentIcon = icon
tokenSelectorPanel.highlightedKey = key ?? ""
}
padding: 10
QtObject {
id: d
property bool isTokenSelected: false
property string currentName
property url currentIcon
}
background: StatusComboboxBackground {
border.width: 0
color: {
if (d.isTokenSelected)
return "transparent"
return root.hovered || dropdown.opened
? Theme.palette.primaryColor2
: Theme.palette.primaryColor3
}
}
contentItem: Loader {
sourceComponent: d.isTokenSelected ? selectedContent
: notSelectedContent
}
Component {
id: notSelectedContent
RowLayout {
spacing: 10
StatusBaseText {
objectName: "tokenSelectorContentItemText"
font.pixelSize: root.font.pixelSize
font.weight: Font.Medium
color: Theme.palette.primaryColor1
text: qsTr("Select token")
}
StatusComboboxIndicator {
color: Theme.palette.primaryColor1
}
}
}
Component {
id: selectedContent
RowLayout {
spacing: Style.current.halfPadding
StatusRoundedImage {
objectName: "tokenSelectorIcon"
Layout.preferredWidth: 20
Layout.preferredHeight: 20
image.source: d.currentIcon
}
StatusBaseText {
objectName: "tokenSelectorContentItemText"
font.pixelSize: 28
color: root.hovered ? Theme.palette.blue : Theme.palette.darkBlue
text: d.currentName
}
StatusComboboxIndicator {
color: Theme.palette.primaryColor1
}
}
}
StatusDropdown {
id: dropdown
y: parent.height + 4
closePolicy: Popup.CloseOnPressOutsideParent
bottomPadding: 0
contentItem: TokenSelectorPanel {
id: tokenSelectorPanel
function findSubitem(key) {
const count = collectiblesModel.rowCount()
for (let i = 0; i < count; i++) {
const entry = ModelUtils.get(collectiblesModel, i)
const subitem = ModelUtils.getByKey(
entry.subitems, "key", key)
if (subitem)
return subitem
}
}
function setCurrentAndClose(name, icon) {
d.currentName = name
d.currentIcon = icon
d.isTokenSelected = true
dropdown.close()
}
onAssetSelected: {
const entry = ModelUtils.getByKey(assetsModel, "tokensKey", key)
highlightedKey = key
setCurrentAndClose(entry.symbol,
Constants.tokenIcon(entry.symbol))
root.assetSelected(key)
}
onCollectibleSelected: {
highlightedKey = key
const subitem = findSubitem(key)
setCurrentAndClose(subitem.name, subitem.icon)
root.collectibleSelected(key)
}
onCollectionSelected: {
highlightedKey = key
const subitem = findSubitem(key)
setCurrentAndClose(subitem.name, subitem.icon)
root.collectionSelected(key)
}
}
}
MouseArea {
anchors.fill: parent
onClicked: dropdown.opened ? dropdown.close() : dropdown.open()
}
}

View File

@ -1,22 +1,23 @@
NetworkFilter 1.0 NetworkFilter.qml
NetworkSelectItemDelegate 1.0 NetworkSelectItemDelegate.qml
AccountHeaderGradient 1.0 AccountHeaderGradient.qml
StatusTxProgressBar 1.0 StatusTxProgressBar.qml
StatusDateRangePicker 1.0 StatusDateRangePicker.qml
ActivityFilterTagItem 1.0 ActivityFilterTagItem.qml
SortOrderComboBox 1.0 SortOrderComboBox.qml
CollectibleBalanceTag 1.0 CollectibleBalanceTag.qml
CollectibleLinksTags 1.0 CollectibleLinksTags.qml
DappsComboBox 1.0 DappsComboBox.qml
EditSlippagePanel 1.0 EditSlippagePanel.qml
FilterComboBox 1.0 FilterComboBox.qml
InformationTileAssetDetails 1.0 InformationTileAssetDetails.qml
ManageTokenMenuButton 1.0 ManageTokenMenuButton.qml
ManageTokensCommunityTag 1.0 ManageTokensCommunityTag.qml
ManageTokensDelegate 1.0 ManageTokensDelegate.qml
ManageTokensGroupDelegate 1.0 ManageTokensGroupDelegate.qml
MaxSendButton 1.0 MaxSendButton.qml
InformationTileAssetDetails 1.0 InformationTileAssetDetails.qml
NetworkFilter 1.0 NetworkFilter.qml
NetworkSelectItemDelegate 1.0 NetworkSelectItemDelegate.qml
SortOrderComboBox 1.0 SortOrderComboBox.qml
StatusDateRangePicker 1.0 StatusDateRangePicker.qml
StatusNetworkListItemTag 1.0 StatusNetworkListItemTag.qml
CollectibleBalanceTag 1.0 CollectibleBalanceTag.qml
CollectibleLinksTags 1.0 CollectibleLinksTags.qml
DappsComboBox 1.0 DappsComboBox.qml
StatusTxProgressBar 1.0 StatusTxProgressBar.qml
SwapExchangeButton 1.0 SwapExchangeButton.qml
EditSlippagePanel 1.0 EditSlippagePanel.qml
TokenSelector 1.0 TokenSelector.qml
SwapModalFooterInfoComponent 1.0 SwapModalFooterInfoComponent.qml
TokenSelector 1.0 TokenSelector.qml
TokenSelectorNew 1.0 TokenSelectorNew.qml

View File

@ -0,0 +1,378 @@
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.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Popups.Dialog 0.1
import AppLayouts.Wallet.views 1.0
import shared.controls 1.0
import utils 1.0
import SortFilterProxyModel 0.2
/**
Two-tabs panel holding searchable lists of assets (single level) and
collectibles (two levels).
Structure:
TabBar (assets, collectibles)
StackLayout (current index bound to tab bar's current index)
Assets List (assets part)
StackView (collectibles part)
Collectibles List (top level - groups by collection/community)
Collectibles List (nested level, on demand)
*/
Control {
id: root
enum Tabs {
Assets = 0,
Collectibles = 1
}
/**
Expected model structure:
tokensKey [string] - unique asset's identifier
name [string] - asset's name
symbol [string] - asset's symbol
iconSource [url] - asset's icon
currencyBalanceAsString [string] - formatted balance
balances [model] - submodel of balances per chain
balanceAsString [string] - formatted balance per chain
iconUrl [url] - chain's icon
**/
property alias assetsModel: assetsSfpm.sourceModel
/**
Expected model structure:
groupName [string] - group name
icon [url] - icon image of a group
type [string] - group type, can be "community" or "other"
subitems [model] - submodel of collectibles/collections of the group
key [string] - balance
name [string] - name of the subitem
balance [int] - balance of the subitem
icon [url] - icon of the subitem
**/
property alias collectiblesModel: collectiblesSfpm.sourceModel
// Index of the current tab, indexes correspond to the Tabs enum values.
property alias currentTab: tabBar.currentIndex
signal assetSelected(string key)
signal collectionSelected(string key)
signal collectibleSelected(string key)
property string highlightedKey: ""
SortFilterProxyModel {
id: assetsSfpm
filters: AnyOf {
SearchFilter {
roleName: "name"
searchPhrase: assetsSearchBox.text
}
SearchFilter {
roleName: "symbol"
searchPhrase: assetsSearchBox.text
}
}
}
SortFilterProxyModel {
id: collectiblesSfpm
filters: SearchFilter {
roleName: "groupName"
searchPhrase: collectiblesSearchBox.text
}
}
component SearchFilter: RegExpFilter {
required property string searchPhrase
pattern: `*${searchPhrase}*`
caseSensitivity : Qt.CaseInsensitive
syntax: RegExpFilter.Wildcard
}
component Search: SearchBox {
input.leftPadding: root.leftPadding
input.rightPadding: root.leftPadding
minimumHeight: 56
maximumHeight: 56
input.showBackground: false
focus: visible
}
contentItem: ColumnLayout {
StatusTabBar {
id: tabBar
visible: !!root.assetsModel && !!root.collectiblesModel
currentIndex: !!root.assetsModel
? TokenSelectorPanel.Tabs.Assets
: TokenSelectorPanel.Tabs.Collectibles
StatusTabButton {
text: qsTr("Assets")
width: implicitWidth
visible: !!root.assetsModel
}
StatusTabButton {
text: qsTr("Collectibles")
width: implicitWidth
visible: !!root.collectiblesModel
}
}
StackLayout {
Layout.maximumHeight: 400
visible: !!root.assetsModel || !!root.collectiblesModel
currentIndex: tabBar.currentIndex
ColumnLayout {
Layout.preferredHeight: visible ? implicitHeight : 0
spacing: 0
Search {
id: assetsSearchBox
Layout.fillWidth: true
placeholderText: qsTr("Search assets")
}
StatusDialogDivider {
Layout.fillWidth: true
visible: assetsListView.count
}
StatusListView {
id: assetsListView
clip: true
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: contentHeight
model: assetsSfpm
delegate: TokenSelectorAssetDelegate {
required property var model
required property int index
highlighted: tokensKey === root.highlightedKey
tokensKey: model.tokensKey
name: model.name
symbol: model.symbol
currencyBalanceAsString: model.currencyBalanceAsString
iconSource: model.iconSource
balancesModel: model.balances
onClicked: root.assetSelected(model.tokensKey)
}
}
}
StackView {
id: collectiblesStackView
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: visible ? currentItem.implicitHeight : 0
initialItem: ColumnLayout {
spacing: 0
Search {
id: collectiblesSearchBox
Layout.fillWidth: true
placeholderText: qsTr("Search collectibles")
}
StatusDialogDivider {
Layout.fillWidth: true
visible: collectiblesListView.count
}
StatusListView {
id: collectiblesListView
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: contentHeight
clip: true
model: collectiblesSfpm
delegate: TokenSelectorCollectibleDelegate {
required property var model
readonly property int subitemsCount:
model.subitems.ModelCount.count
readonly property bool isCommunity:
model.type === "community"
readonly property bool showCount:
subitemsCount > 1 || isCommunity
name: model.groupName
balance: showCount ? subitemsCount : ""
image: model.icon
goDeeperIconVisible: subitemsCount > 1
|| isCommunity
highlighted: subitemsCount === 1 && !isCommunity
? ModelUtils.get(model.subitems, 0, "key")
=== root.highlightedKey
: false
onClicked: {
if (subitemsCount === 1 && !isCommunity) {
const key = ModelUtils.get(model.subitems, 0, "key")
root.collectibleSelected(key)
return
}
const parameters = {
index: collectiblesSfpm.index(model.index, 0),
model: model.subitems,
isCommunity: isCommunity
}
collectiblesStackView.push(
collectiblesSublistComponent,
parameters,
StackView.Immediate)
}
}
section.property: "type"
section.delegate: StatusBaseText {
id: sectionTitle
color: Theme.palette.baseColor1
topPadding: Style.current.padding
text: section === "community"
? qsTr("Community minted")
: qsTr("Other")
}
}
}
}
}
}
Component {
id: collectiblesSublistComponent
ColumnLayout {
property var index
property alias model: sublistSfpm.sourceModel
property bool isCommunity
spacing: 0
SortFilterProxyModel {
id: sublistSfpm
filters: SearchFilter {
roleName: "name"
searchPhrase: collectiblesSublistSearchBox.text
}
}
StatusIconTextButton {
id: backButton
statusIcon: "previous"
icon.width: 12
icon.height: 12
text: qsTr("Back")
onClicked: collectiblesStackView.pop(StackView.Immediate)
}
StatusDialogDivider {
Layout.fillWidth: true
visible: collectiblesListView.count
}
Search {
id: collectiblesSublistSearchBox
Layout.fillWidth: true
placeholderText: qsTr("Search collectibles")
}
StatusDialogDivider {
Layout.fillWidth: true
visible: collectiblesListView.count
}
StatusListView {
id: sublist
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: contentHeight
model: sublistSfpm
clip: true
delegate: TokenSelectorCollectibleDelegate {
required property var model
name: model.name
balance: model.balance > 1 ? model.balance : ""
image: model.icon
goDeeperIconVisible: false
highlighted: model.key === root.highlightedKey
onClicked: {
if (isCommunity)
root.collectionSelected(model.key)
else
root.collectibleSelected(model.key)
}
}
}
// Detection if the related model entry has been removed.
// Using model.Component.destruction.connect is not reliable because
// is not called for submodels maintained in c++ by the parent model.
ItemSelectionModel {
id: selection
model: collectiblesSfpm
onHasSelectionChanged: {
if (!hasSelection)
collectiblesStackView.pop(StackView.Immediate)
}
Component.onCompleted: select(index, ItemSelectionModel.Select)
}
}
}
}

View File

@ -1,11 +1,12 @@
WalletHeader 1.0 WalletHeader.qml
WalletTxProgressBlock 1.0 WalletTxProgressBlock.qml
WalletNftPreview 1.0 WalletNftPreview.qml
ActivityFilterPanel 1.0 ActivityFilterPanel.qml
ContractInfoButtonWithMenu 1.0 ContractInfoButtonWithMenu.qml
DAppsWorkflow 1.0 DAppsWorkflow.qml
ManageAssetsPanel 1.0 ManageAssetsPanel.qml
ManageCollectiblesPanel 1.0 ManageCollectiblesPanel.qml
ManageHiddenPanel 1.0 ManageHiddenPanel.qml
DAppsWorkflow 1.0 DAppsWorkflow.qml
SwapInputPanel 1.0 SwapInputPanel.qml
ContractInfoButtonWithMenu 1.0 ContractInfoButtonWithMenu.qml
SignInfoBox 1.0 SignInfoBox.qml
SwapInputPanel 1.0 SwapInputPanel.qml
TokenSelectorPanel 1.0 TokenSelectorPanel.qml
WalletHeader 1.0 WalletHeader.qml
WalletNftPreview 1.0 WalletNftPreview.qml
WalletTxProgressBlock 1.0 WalletTxProgressBlock.qml

View File

@ -18,6 +18,8 @@ QtObject {
readonly property var _allCollectiblesModel: !!root._allCollectiblesModule ? root._allCollectiblesModule.allCollectiblesModel : null
readonly property var allCollectiblesModel: RolesRenamingModel {
objectName: "allCollectiblesModel"
sourceModel: root._allCollectiblesModel
mapping: [
@ -85,6 +87,8 @@ QtObject {
/* PRIVATE: This model joins the "Tokens By Symbol Model" and "Communities Model" by communityId */
property LeftJoinModel _jointCollectiblesBySymbolModel: LeftJoinModel {
objectName: "jointCollectiblesBySymbolModel"
leftModel: allCollectiblesModel
rightModel: _renamedCommunitiesModel
joinRole: "communityId"

View File

@ -0,0 +1,100 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
ItemDelegate {
id: root
required property string name
required property string balance
required property url image
property bool goDeeperIconVisible: true
property bool interactive: true
spacing: Style.current.halfPadding
horizontalPadding: Style.current.padding
verticalPadding: 4
opacity: interactive ? 1 : 0.3
implicitWidth: ListView.view.width
implicitHeight: 60
icon.width: 32
icon.height: 32
icon.source: root.image
enabled: interactive
background: Rectangle {
radius: Style.current.radius
color: (root.interactive && root.hovered) || root.highlighted
? Theme.palette.statusListItem.highlightColor
: "transparent"
HoverHandler {
cursorShape: root.interactive ? Qt.PointingHandCursor : undefined
}
}
contentItem: RowLayout {
spacing: root.spacing
// asset icon
StatusRoundedImage {
Layout.preferredWidth: root.icon.width
Layout.preferredHeight: root.icon.height
image.source: root.icon.source
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
// name, symbol, total balance
RowLayout {
Layout.fillWidth: true
spacing: root.spacing
StatusBaseText {
id: nameText
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: root.name
font.weight: Font.Medium
elide: Text.ElideRight
}
StatusBaseText {
Layout.alignment: Qt.AlignVCenter
text: root.balance
visible: root.balance !== ""
color: Theme.palette.baseColor1
font.pixelSize: 13
font.weight: Font.Medium
elide: Text.ElideRight
}
StatusIcon {
Layout.alignment: Qt.AlignVCenter
icon: "tiny/chevron-right"
visible: root.goDeeperIconVisible
color: Theme.palette.baseColor1
width: 16
height: 16
}
}
}
}
}

View File

@ -4,4 +4,5 @@ NetworkSelectionView 1.0 NetworkSelectionView.qml
NetworkSelectorView 1.0 NetworkSelectorView.qml
SavedAddresses 1.0 SavedAddresses.qml
TokenSelectorAssetDelegate 1.0 TokenSelectorAssetDelegate.qml
TokenSelectorCollectibleDelegate 1.0 TokenSelectorCollectibleDelegate.qml
TokenSelectorView 1.0 TokenSelectorView.qml

View File

@ -90,6 +90,8 @@ QObject {
ObjectProxyModel {
id: proxyModel
objectName: "assetsViewAdaptorProxyModel"
sourceModel: root.tokensModel ?? null
delegate: QObject {