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() } } } } }