status-desktop/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleDetailView.qml

409 lines
18 KiB
QML

import QtQuick 2.14
import QtQuick.Layouts 1.13
import SortFilterProxyModel 0.2
import StatusQ 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import AppLayouts.Communities.panels 1.0
import utils 1.0
import shared.controls 1.0
import shared.views 1.0
import shared.popups 1.0
import "../../stores"
import "../../controls"
Item {
id: root
signal launchTransactionDetail(string txID)
required property var rootStore
required property var walletRootStore
required property var communitiesStore
required property var collectible
property var activityModel
property bool isCollectibleLoading
required property string addressFilters
// Community related token props:
readonly property bool isCommunityCollectible: !!collectible ? collectible.communityId !== "" : false
readonly property bool isOwnerTokenType: !!collectible ? (collectible.communityPrivilegesLevel === Constants.TokenPrivilegesLevel.Owner) : false
readonly property bool isTMasterTokenType: !!collectible ? (collectible.communityPrivilegesLevel === Constants.TokenPrivilegesLevel.TMaster) : false
readonly property var communityDetails: isCommunityCollectible ? root.communitiesStore.getCommunityDetailsAsJson(collectible.communityId) : null
QtObject {
id: d
readonly property string collectibleLink: !!collectible ? root.walletRootStore.getOpenSeaCollectibleUrl(collectible.networkShortName, collectible.contractAddress, collectible.tokenId): ""
readonly property string collectionLink: !!collectible ? root.walletRootStore.getOpenSeaCollectionUrl(collectible.networkShortName, collectible.contractAddress): ""
readonly property string blockExplorerLink: !!collectible ? root.walletRootStore.getExplorerUrl(collectible.networkShortName, collectible.contractAddress, collectible.tokenId): ""
readonly property var addrFilters: root.addressFilters.split(":").map((addr) => addr.toLowerCase())
readonly property int imageStackSpacing: 4
property bool activityLoading: walletRootStore.tmpActivityController0.status.loadingData
property Component balanceTag: Component {
CollectibleBalanceTag {
balance: d.balanceAggregator.value
}
}
property SortFilterProxyModel filteredBalances: SortFilterProxyModel {
sourceModel: !!collectible ? collectible.ownership : null
filters: [
FastExpressionFilter {
expression: {
d.addrFilters
return d.addrFilters.includes(model.accountAddress.toLowerCase())
}
expectedRoles: ["accountAddress"]
}
]
}
property SumAggregator balanceAggregator: SumAggregator {
model: d.filteredBalances
roleName: "balance"
}
function getCurrentTab() {
for (let i =0; i< collectiblesDetailsTab.contentChildren.length; i++) {
if(collectiblesDetailsTab.contentChildren[i].visible) {
return i
}
}
return 0
}
}
CollectibleDetailsHeader {
id: collectibleHeader
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
collectibleName: !!collectible && !!collectible.name ? collectible.name : qsTr("Unknown")
collectibleId: !!collectible ? "#" + collectible.tokenId : ""
communityName: !!communityDetails ? communityDetails.name : ""
communityId: !!collectible ? collectible.communityId : ""
collectionName: !!collectible ? collectible.collectionName : ""
communityImage: !!communityDetails ? communityDetails.image : ""
networkShortName: !!collectible ? collectible.networkShortName : ""
networkColor: !!collectible ?collectible.networkColor : ""
networkIconURL: !!collectible ? collectible.networkIconUrl : ""
networkExplorerName: !!collectible ? root.walletRootStore.getExplorerNameForNetwork(collectible.networkShortName): ""
collectibleLinkEnabled: Utils.getUrlStatus(d.collectibleLink)
collectionLinkEnabled: (!!communityDetails && communityDetails.name) || Utils.getUrlStatus(d.collectionLink)
explorerLinkEnabled: Utils.getUrlStatus(d.blockExplorerLink)
onCollectionTagClicked: {
if (root.isCommunityCollectible) {
Global.switchToCommunity(collectible.communityId)
}
else {
Global.openLinkWithConfirmation(d.collectionLink, root.walletRootStore.getOpenseaDomainName())
}
}
onOpenCollectibleExternally: Global.openLinkWithConfirmation(d.collectibleLink, root.walletRootStore.getOpenseaDomainName())
onOpenCollectibleOnExplorer: Global.openLinkWithConfirmation(d.blockExplorerLink, root.walletRootStore.getExplorerDomain(networkShortName))
}
ColumnLayout {
id: collectibleBody
anchors.top: collectibleHeader.bottom
anchors.topMargin: 25
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
spacing: Style.current.padding
Row {
id: collectibleImageDetails
readonly property real visibleImageHeight: artwork.height
readonly property real visibleImageWidth: artwork.width
Layout.preferredHeight: collectibleImageDetails.visibleImageHeight
Layout.fillWidth: true
spacing: 24
ColumnLayout {
id: artwork
spacing: 0
Repeater {
id: repeater
model: Math.min(3, d.balanceAggregator.value)
Item {
Layout.preferredWidth: childrenRect.width
Layout.preferredHeight: childrenRect.height
Layout.leftMargin: index * d.imageStackSpacing
Layout.topMargin: index === 0 ? 0 : -Layout.preferredHeight + d.imageStackSpacing
opacity: index === 0 ? 1: 0.4/index
// so that the first item remains on top in the stack
z: -index
Loader {
property int modelIndex: index
anchors.top: parent.top
anchors.left: parent.left
sourceComponent: root.isCollectibleLoading ?
collectibleimageComponent:
root.isCommunityCollectible && (root.isOwnerTokenType || root.isTMasterTokenType) ?
privilegedCollectibleImageComponent:
collectibleimageComponent
active: root.visible
}
Loader {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Style.current.padding
sourceComponent: d.balanceTag
// only show balance tag on top of the first image in stack
active: index === 0 && d.balanceAggregator.value > 1 && root.visible
}
}
}
}
Column {
id: collectibleNameAndDescription
spacing: 12
width: parent.width - collectibleImageDetails.visibleImageWidth - Style.current.bigPadding
StatusBaseText {
id: collectibleName
width: parent.width
height: 24
text: root.isCommunityCollectible && !!communityDetails ? qsTr("Minted by %1").arg(root.communityDetails.name):
!!collectible ? collectible.collectionName: ""
color: Theme.palette.directColor1
font.pixelSize: 17
lineHeight: 24
lineHeightMode: Text.FixedHeight
elide: Text.ElideRight
wrapMode: Text.WordWrap
}
StatusBaseText {
id: descriptionText
width: parent.width
height: collectibleImageDetails.height - collectibleName.height - parent.spacing
clip: true
text: !!collectible ? collectible.description : ""
textFormat: Text.MarkdownText
color: Theme.palette.directColor4
font.pixelSize: 15
lineHeight: 22
lineHeightMode: Text.FixedHeight
elide: Text.ElideRight
wrapMode: Text.Wrap
}
}
}
StatusTabBar {
id: collectiblesDetailsTab
Layout.fillWidth: true
topPadding: 52
currentIndex: d.getCurrentTab()
StatusTabButton {
text: qsTr("Properties")
width: visible ? implicitWidth: 0
visible: root.isCommunityCollectible
enabled: visible
}
StatusTabButton {
text: qsTr("Traits")
width: visible ? implicitWidth: 0
visible: !root.isCommunityCollectible && !!collectible && collectible.traits.count > 0
enabled: visible
}
StatusTabButton {
text: qsTr("Activity")
width: visible ? implicitWidth: 0
}
StatusTabButton {
text: qsTr("Links")
width: visible ? implicitWidth: 0
visible: !root.isCommunityCollectible && (!!collectible &&
((!!collectible.website && !!collectible.collectionName) ||
collectible.twitterHandle))
enabled: visible
}
}
StatusScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
contentWidth: availableWidth
padding: 0
Loader {
id: tabLoader
width: scrollView.availableWidth
sourceComponent: {
switch (collectiblesDetailsTab.currentIndex) {
case 0: return traitsView
case 1: return traitsView
case 2: return activityView
case 3: return linksView
}
}
Component {
id: traitsView
Flow {
spacing: 10
Repeater {
model: !!collectible ? collectible.traits: null
InformationTile {
maxWidth: parent.width
primaryText: model.traitType
secondaryText: model.value
}
}
}
}
Component {
id: activityView
StatusListView {
height: scrollView.availableHeight
model: root.activityModel
header: ShapeRectangle {
width: parent.width
height: visible ? 42: 0
visible: !root.activityModel.count && !d.activityLoading
font.pixelSize: Style.current.primaryTextFontSize
text: qsTr("Activity will appear here")
}
delegate: TransactionDelegate {
required property var model
required property int index
width: parent.width
modelData: model.activityEntry
timeStampText: isModelDataValid ? LocaleUtils.formatRelativeTimestamp(modelData.timestamp * 1000, true) : ""
rootStore: root.rootStore
walletRootStore: root.walletRootStore
showAllAccounts: root.walletRootStore.showAllAccounts
displayValues: true
community: isModelDataValid && !!communityId && !!root.communitiesStore ? root.communitiesStore.getCommunityDetailsAsJson(communityId) : null
loading: false
onClicked: {
if (mouse.button === Qt.RightButton) {
// TODO: Implement context menu
} else {
root.launchTransactionDetail(modelData.id)
}
}
}
}
}
Component {
id: linksView
Flow {
spacing: 10
CollectibleLinksTags {
asset.name: !!collectible ? collectible.collectionImageUrl: ""
asset.isImage: true
primaryText: !!collectible ? collectible.collectionName : ""
secondaryText: !!collectible ? collectible.website : ""
visible: !!collectible && !!collectible.website && !!collectible.collectionName
enabled: !!collectible ? Utils.getUrlStatus(collectible.website): false
onClicked: Global.openLinkWithConfirmation(collectible.website, collectible.website)
}
CollectibleLinksTags {
asset.name: "tiny/opensea"
primaryText: qsTr("Opensea")
secondaryText: d.collectionLink
visible: Utils.getUrlStatus(d.collectionLink)
onClicked: Global.openLinkWithConfirmation(d.collectionLink, root.walletRootStore.getOpenseaDomainName())
}
CollectibleLinksTags {
asset.name: "xtwitter"
primaryText: qsTr("Twitter")
secondaryText: !!collectible ? collectible.twitterHandle : ""
visible: !!collectible && collectible.twitterHandle
onClicked: Global.openLinkWithConfirmation(root.walletRootStore.getTwitterLink(collectible.twitterHandle), Constants.socialLinkPrefixesByType[Constants.socialLinkType.twitter])
}
}
}
}
}
}
Component {
id: privilegedCollectibleImageComponent
// Special artwork representation for community `Owner and Master Token` token types:
PrivilegedTokenArtworkPanel {
size: PrivilegedTokenArtworkPanel.Size.Large
artwork: collectible.imageUrl ?? ""
color: !!root.collectible && !!root.communityDetails ? root.communityDetails.color : "transparent"
isOwner: root.isOwnerTokenType
}
}
Component {
id: collectibleimageComponent
StatusRoundedMedia {
id: collectibleImage
readonly property bool isEmpty: !mediaUrl.toString() && !fallbackImageUrl.toString()
radius: Style.current.radius
color: isError || isEmpty ? Theme.palette.baseColor5 : collectible.backgroundColor
mediaUrl: collectible.mediaUrl ?? ""
mediaType: !!collectible ? (modelIndex > 0 && collectible.mediaType.startsWith("video")) ? "" : collectible.mediaType: ""
fallbackImageUrl: collectible.imageUrl
manualMaxDimension: 240
interactive: !isError && !isEmpty
enabled: interactive
onImageClicked: (image, plain) => Global.openImagePopup(image, "", plain)
onVideoClicked: (url) => Global.openVideoPopup(url)
onOpenImageContextMenu: (url, isGif) => Global.openMenu(imageContextMenu, collectibleImage, { imageSource: url, isGif: isGif, isVideo: false })
onOpenVideoContextMenu: (url) => Global.openMenu(imageContextMenu, collectibleImage, { imageSource: url, url: url, isVideo: true, isGif: false })
Loader {
anchors.fill: parent
active: collectibleImage.isLoading
sourceComponent: LoadingComponent {radius: collectibleImage.radius}
}
Loader {
anchors.fill: parent
active: collectibleImage.isError || collectibleImage.isEmpty
sourceComponent: LoadingErrorComponent {
radius: collectibleImage.radius
text: {
if (collectibleImage.isError && collectibleImage.componentMediaType === StatusRoundedMedia.MediaType.Unkown) {
return qsTr("Unsupported\nfile format")
}
if (!collectible.description && !collectible.name) {
return qsTr("Info can't\nbe fetched")
}
return qsTr("Failed\nto load")
}
}
}
Component {
id: imageContextMenu
ImageContextMenu {
onClosed: destroy()
}
}
}
}
}