diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index c76f5a428a..2d5f77f006 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -662,7 +662,7 @@ Rectangle { anchors.right: parent.right; text: "Cancel" onClicked: { - sendToScript({method: 'checkout_cancelClicked', params: itemId}); + sendToScript({method: 'checkout_cancelClicked', itemId: itemId}); } } } diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 0d0af875d1..759d61b924 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -24,11 +24,8 @@ Item { HifiConstants { id: hifi; } id: root; - property string referrerURL: (Account.metaverseServerURL + "/marketplace?"); - readonly property int additionalDropdownHeight: usernameDropdown.height - myUsernameButton.anchors.bottomMargin; - property alias usernameDropdownVisible: usernameDropdown.visible; - height: mainContainer.height + additionalDropdownHeight; + height: mainContainer.height; Connections { target: Commerce; @@ -93,77 +90,7 @@ Item { MouseArea { anchors.fill: parent; onClicked: { - sendToParent({method: "header_marketplaceImageClicked", referrerURL: root.referrerURL}); - } - } - } - - Item { - id: buttonAndUsernameContainer; - anchors.left: marketplaceHeaderImage.right; - anchors.leftMargin: 8; - anchors.top: parent.top; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 10; - anchors.right: securityImage.left; - anchors.rightMargin: 6; - - TextMetrics { - id: textMetrics; - font.family: "Raleway" - text: usernameText.text; - } - - Rectangle { - id: myUsernameButton; - anchors.right: parent.right; - anchors.verticalCenter: parent.verticalCenter; - height: 40; - width: usernameText.width + 25; - color: "white"; - radius: 4; - border.width: 1; - border.color: hifi.colors.lightGray; - - // Username Text - RalewayRegular { - id: usernameText; - text: Account.username; - // Text size - size: 18; - // Style - color: hifi.colors.baseGray; - elide: Text.ElideRight; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - width: Math.min(textMetrics.width + 25, 110); - // Anchors - anchors.centerIn: parent; - rightPadding: 10; - } - - HiFiGlyphs { - id: dropdownIcon; - text: hifi.glyphs.caratDn; - // Size - size: 50; - // Anchors - anchors.right: parent.right; - anchors.rightMargin: -14; - anchors.verticalCenter: parent.verticalCenter; - horizontalAlignment: Text.AlignHCenter; - // Style - color: hifi.colors.baseGray; - } - - MouseArea { - anchors.fill: parent; - hoverEnabled: enabled; - onClicked: { - usernameDropdown.visible = !usernameDropdown.visible; - } - onEntered: usernameText.color = hifi.colors.baseGrayShadow; - onExited: usernameText.color = hifi.colors.baseGray; + sendToParent({method: "header_marketplaceImageClicked"}); } } } @@ -205,92 +132,6 @@ Item { } } - Item { - id: usernameDropdown; - z: 998; - visible: false; - anchors.top: buttonAndUsernameContainer.bottom; - anchors.topMargin: -buttonAndUsernameContainer.anchors.bottomMargin; - anchors.right: buttonAndUsernameContainer.right; - height: childrenRect.height; - width: 150; - - Rectangle { - id: myItemsButton; - color: hifi.colors.white; - anchors.top: parent.top; - anchors.left: parent.left; - anchors.right: parent.right; - height: 50; - - RalewaySemiBold { - anchors.fill: parent; - text: "My Submissions" - color: hifi.colors.baseGray; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - size: 18; - } - - MouseArea { - anchors.fill: parent; - hoverEnabled: true; - onEntered: { - myItemsButton.color = hifi.colors.blueHighlight; - } - onExited: { - myItemsButton.color = hifi.colors.white; - } - onClicked: { - sendToParent({method: "header_myItemsClicked"}); - } - } - } - - Rectangle { - id: logOutButton; - color: hifi.colors.white; - anchors.top: myItemsButton.bottom; - anchors.left: parent.left; - anchors.right: parent.right; - height: 50; - - RalewaySemiBold { - anchors.fill: parent; - text: "Log Out" - color: hifi.colors.baseGray; - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - size: 18; - } - - MouseArea { - anchors.fill: parent; - hoverEnabled: true; - onEntered: { - logOutButton.color = hifi.colors.blueHighlight; - } - onExited: { - logOutButton.color = hifi.colors.white; - } - onClicked: { - Account.logOut(); - } - } - } - } - - DropShadow { - z: 997; - visible: usernameDropdown.visible; - anchors.fill: usernameDropdown; - horizontalOffset: 3; - verticalOffset: 3; - radius: 8.0; - samples: 17; - color: "#80000000"; - source: usernameDropdown; - } } diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index 232e17d851..8ca34af28a 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -22,7 +22,6 @@ Rectangle { HifiConstants { id: hifi; } id: root; - property string marketplaceUrl: ""; property string entityId: ""; property string certificateId: ""; property string itemName: "--"; @@ -30,6 +29,7 @@ Rectangle { property string itemEdition: "--"; property string dateAcquired: "--"; property string itemCost: "--"; + property string marketplace_item_id: ""; property string certTitleTextColor: hifi.colors.darkGray; property string certTextColor: hifi.colors.white; property string infoTextColor: hifi.colors.blueAccent; @@ -69,7 +69,7 @@ Rectangle { errorText.text = "Information about this certificate is currently unavailable. Please try again later."; } } else { - root.marketplaceUrl = result.data.marketplace_item_url; + root.marketplace_item_id = result.data.marketplace_item_id; root.isMyCert = result.isMyCert ? result.isMyCert : false; if (root.certInfoReplaceMode > 3) { @@ -352,7 +352,7 @@ Rectangle { anchors.fill: parent; hoverEnabled: enabled; onClicked: { - sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', marketplaceUrl: root.marketplaceUrl}); + sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', itemId: root.marketplace_item_id}); } onEntered: itemName.color = hifi.colors.blueHighlight; onExited: itemName.color = root.certTextColor; @@ -391,7 +391,7 @@ Rectangle { // "Show In Marketplace" button HifiControlsUit.Button { id: showInMarketplaceButton; - enabled: root.marketplaceUrl; + enabled: root.marketplace_item_id && marketplace_item_id !== ""; color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.light; anchors.bottom: parent.bottom; @@ -401,7 +401,7 @@ Rectangle { height: 40; text: "View In Market" onClicked: { - sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', marketplaceUrl: root.marketplaceUrl}); + sendToScript({method: 'inspectionCertificate_showInMarketplaceClicked', itemId: root.marketplace_item_id}); } } } @@ -620,7 +620,7 @@ Rectangle { root.itemOwner = "--"; root.itemEdition = "--"; root.dateAcquired = "--"; - root.marketplaceUrl = ""; + root.marketplace_item_id = ""; root.itemCost = "--"; root.isMyCert = false; errorText.text = ""; diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml new file mode 100644 index 0000000000..351c43286c --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -0,0 +1,1216 @@ +// +// Marketplace.qml +// qml/hifi/commerce/marketplace +// +// Marketplace +// +// Created by Roxanne Skelly on 2019-01-18 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "../common/sendAsset" +import "../.." as HifiCommon + +Rectangle { + HifiConstants { + id: hifi + } + + id: root + + property string activeView: "initialize" + property int currentSortIndex: 0 + property string sortString: "recent" + property string categoryString: "" + property string searchString: "" + property bool keyboardEnabled: HMD.active + property bool keyboardRaised: false + property string searchScopeString: "Featured" + property bool isLoggedIn: false; + property bool supports3DHTML: true; + + anchors.fill: (typeof parent === undefined) ? undefined : parent + + function getMarketplaceItems() { + marketplaceItemView.visible = false; + itemsList.visible = true; + licenseInfo.visible = false; + marketBrowseModel.getFirstPage(); + { + if(root.searchString !== undefined && root.searchString !== "") { + root.searchScopeString = "Search Results: \"" + root.searchString + "\""; + } else if (root.categoryString !== "") { + root.searchScopeString = root.categoryString; + } else { + root.searchScopeString = "Featured"; + } + } + } + + Component.onCompleted: { + Commerce.getLoginStatus(); + + supports3DHTML = PlatformInfo.has3DHTML(); + } + + Component.onDestruction: { + keyboard.raised = false; + } + + Connections { + target: GlobalServices + + onMyUsernameChanged: { + Commerce.getLoginStatus(); + } + } + + Connections { + target: MarketplaceScriptingInterface + + onGetMarketplaceCategoriesResult: { + if (result.status !== 'success') { + console.log("Failed to get Marketplace Categories", result.data.message); + } else { + categoriesModel.clear(); + categoriesModel.append({ + id: -1, + name: "Everything" + }); + result.data.items.forEach(function(category) { + categoriesModel.append({ + id: category.id, + name: category.name + }); + }); + } + getMarketplaceItems(); + } + onGetMarketplaceItemsResult: { + marketBrowseModel.handlePage(result.status !== "success" && result.message, result); + } + + onGetMarketplaceItemResult: { + if (result.status !== 'success') { + console.log("Failed to get Marketplace Item", result.data.message); + } else { + marketplaceItem.supports3DHTML = root.supports3DHTML; + marketplaceItem.item_id = result.data.id; + marketplaceItem.image_url = result.data.thumbnail_url; + marketplaceItem.name = result.data.title; + marketplaceItem.likes = result.data.likes; + if(result.data.has_liked !== undefined) { + marketplaceItem.liked = result.data.has_liked; + } + marketplaceItem.creator = result.data.creator; + marketplaceItem.categories = result.data.categories; + marketplaceItem.price = result.data.cost; + marketplaceItem.description = result.data.description; + marketplaceItem.attributions = result.data.attributions; + marketplaceItem.license = result.data.license; + marketplaceItem.available = result.data.availability === "available"; + marketplaceItem.created_at = result.data.created_at; + marketplaceItemScrollView.contentHeight = marketplaceItemContent.height; + itemsList.visible = false; + marketplaceItemView.visible = true; + } + } + } + + Connections { + target: Commerce + + onLoginStatusResult: { + root.isLoggedIn = isLoggedIn; + } + } + + HifiCommerceCommon.CommerceLightbox { + id: lightboxPopup + visible: false + anchors.fill: parent + } + + // + // HEADER BAR START + // + + Rectangle { + id: headerShadowBase + anchors.fill: header + color: "white" + } + DropShadow { + anchors.fill: headerShadowBase + source: headerShadowBase + verticalOffset: 4 + horizontalOffset: 4 + radius: 6 + samples: 9 + color: hifi.colors.baseGrayShadow + z:5 + } + Rectangle { + id: header; + + visible: true + anchors { + left: parent.left + top: parent.top + right: parent.right + } + + height: childrenRect.height+5 + z: 5 + + Rectangle { + id: titleBarContainer + + anchors.left: parent.left + anchors.top: parent.top + width: parent.width + height: 60 + visible: true + + Image { + id: marketplaceHeaderImage; + source: "../common/images/marketplaceHeaderImage.png"; + anchors.top: parent.top; + anchors.topMargin: 2; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 0; + anchors.left: parent.left; + anchors.leftMargin: 8; + width: 140; + fillMode: Image.PreserveAspectFit; + + MouseArea { + anchors.fill: parent; + onClicked: { + sendToParent({method: "header_marketplaceImageClicked"}); + } + } + } + } + + Rectangle { + id: searchBarContainer + + anchors.top: titleBarContainer.bottom + width: parent.width + height: 50 + + visible: true + clip: false + + // + // TODO: Possibly change this to be a combo box + // + Rectangle { + id: categoriesButton + + anchors { + left: parent.left + leftMargin: 10 + verticalCenter: parent.verticalCenter + } + height: 34 + width: categoriesText.width + 25 + + color: hifi.colors.white + radius: 4 + border.width: 1 + border.color: hifi.colors.lightGrayText + + RalewayRegular { + id: categoriesText + + anchors.centerIn: parent + + text: "Categories" + size: 14 + color: hifi.colors.lightGrayText + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + rightPadding: 10 + } + + HiFiGlyphs { + id: categoriesDropdownIcon + + anchors { + right: parent.right + rightMargin: -8 + verticalCenter: parent.verticalCenter + } + + text: hifi.glyphs.caratDn + size: 34 + horizontalAlignment: Text.AlignHCenter + color: hifi.colors.baseGray + } + + MouseArea { + anchors.fill: parent + + hoverEnabled: enabled + onClicked: { + categoriesDropdown.visible = !categoriesDropdown.visible; + categoriesButton.color = categoriesDropdown.visible ? hifi.colors.lightGray : hifi.colors.white; + categoriesDropdown.forceActiveFocus(); + } + onEntered: categoriesText.color = hifi.colors.baseGrayShadow + onExited: categoriesText.color = hifi.colors.baseGray + } + + Component.onCompleted: { + MarketplaceScriptingInterface.getMarketplaceCategories(); + } + } + + // or + RalewayRegular { + id: orText + + anchors.left: categoriesButton.right + anchors.verticalCenter: parent.verticalCenter + width: 25 + + text: "or" + size: 18; + color: hifi.colors.baseGray + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + rightPadding: 10 + leftPadding: 10 + } + + HifiControlsUit.TextField { + id: searchField + + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + left: orText.right + rightMargin: 10 + } + height: 34 + + isSearchField: true + colorScheme: hifi.colorSchemes.faintGray + font.family: "Fira Sans" + font.pixelSize: hifi.fontSizes.textFieldInput + placeholderText: "Search Marketplace" + + // workaround for https://bugreports.qt.io/browse/QTBUG-49297 + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Return: + case Qt.Key_Enter: + event.accepted = true; + + // emit accepted signal manually + if (acceptableInput) { + searchField.accepted(); + searchField.forceActiveFocus(); + } + break; + case Qt.Key_Backspace: + if (searchField.text === "") { + primaryFilter_index = -1; + } + break; + } + } + + onAccepted: { + root.searchString = searchField.text; + getMarketplaceItems(); + searchField.forceActiveFocus(); + } + + onActiveFocusChanged: { + root.keyboardRaised = activeFocus; + } + + } + } + } + // + // HEADER BAR END + // + + // + // CATEGORIES LIST START + // + Item { + id: categoriesDropdown + + anchors.fill: parent; + + visible: false + z: 10 + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + onClicked: { + categoriesDropdown.visible = false; + categoriesButton.color = hifi.colors.white; + } + } + + Rectangle { + anchors { + left: parent.left; + bottom: parent.bottom; + top: parent.top; + topMargin: 100; + } + width: parent.width/3 + + color: hifi.colors.white + + ListModel { + id: categoriesModel + } + + ListView { + id: categoriesListView; + + anchors.fill: parent + anchors.rightMargin: 10 + width: parent.width + currentIndex: -1; + clip: true + + model: categoriesModel + delegate: ItemDelegate { + height: 34 + width: parent.width + + clip: true + contentItem: Rectangle { + id: categoriesItem + + anchors.fill: parent + + color: hifi.colors.white + visible: true + + RalewayRegular { + id: categoriesItemText + + anchors.leftMargin: 15 + anchors.fill:parent + + text: model.name + color: ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.baseGray + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 14 + } + } + + MouseArea { + anchors.fill: parent + z: 10 + + hoverEnabled: true + propagateComposedEvents: false + + onEntered: { + categoriesItem.color = ListView.isCurrentItem ? hifi.colors.white : hifi.colors.lightBlueHighlight; + } + + onExited: { + categoriesItem.color = ListView.isCurrentItem ? hifi.colors.lightBlueHighlight : hifi.colors.white; + } + + onClicked: { + categoriesListView.currentIndex = index; + categoriesText.text = categoriesItemText.text; + categoriesDropdown.visible = false; + categoriesButton.color = hifi.colors.white; + root.categoryString = categoriesItemText.text; + getMarketplaceItems(); + } + } + } + + ScrollBar.vertical: ScrollBar { + parent: categoriesListView.parent + + anchors { + top: categoriesListView.top; + bottom: categoriesListView.bottom; + left: categoriesListView.right; + } + + contentItem.opacity: 1 + } + } + } + } + // + // CATEGORIES LIST END + // + + // + // ITEMS LIST START + // + Item { + id: itemsList + + anchors { + fill: parent + topMargin: 115 + bottomMargin: 50 + } + + visible: true + + HifiModels.PSFListModel { + id: marketBrowseModel + + itemsPerPage: 7 + listModelName: 'marketBrowse' + listView: marketBrowseList + + getPage: function () { + MarketplaceScriptingInterface.getMarketplaceItems( + root.searchString === "" ? undefined : root.searchString, + "", + root.categoryString.toLowerCase(), + "", + "", + root.sortString, + false, + marketBrowseModel.currentPageToRetrieve, + marketBrowseModel.itemsPerPage + ); + } + + processPage: function(data) { + return data.items; + } + } + ListView { + id: marketBrowseList + + anchors.fill: parent + anchors.rightMargin: 10 + + model: marketBrowseModel + + orientation: ListView.Vertical + focus: true + clip: true + + delegate: MarketplaceListItem { + item_id: model.id + + anchors.topMargin: 10 + + image_url:model.thumbnail_url + name: model.title + likes: model.likes + liked: model.has_liked ? model.has_liked : false + creator: model.creator + category: model.primary_category + price: model.cost + available: model.availability === "available" + isLoggedIn: root.isLoggedIn; + + onShowItem: { + MarketplaceScriptingInterface.getMarketplaceItem(item_id); + } + + onBuy: { + sendToScript({method: 'marketplace_checkout', itemId: item_id}); + } + + onCategoryClicked: { + root.categoryString = category; + categoriesText.text = category; + getMarketplaceItems(); + } + + onRequestReload: getMarketplaceItems(); + } + + ScrollBar.vertical: ScrollBar { + parent: marketBrowseList.parent + + anchors { + top: marketBrowseList.top + bottom: marketBrowseList.bottom + left: marketBrowseList.right + } + + contentItem.opacity: 1 + } + + headerPositioning: ListView.InlineHeader + + header: Item { + id: itemsHeading + + height: childrenRect.height + width: parent.width + + Rectangle { + id: itemsSpacer; + height: 20 + } + + Rectangle { + id: itemsLoginStatus; + anchors { + top: itemsSpacer.bottom + left: parent.left + right: parent.right + leftMargin: 15 + } + height: root.isLoggedIn ? 0 : 80 + + visible: !root.isLoggedIn + color: hifi.colors.greenHighlight + border.color: hifi.colors.greenShadow + border.width: 1 + radius: 4 + z: 10000 + + HifiControlsUit.Button { + id: loginButton; + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + leftMargin: 15 + topMargin:10 + bottomMargin: 10 + } + width: 80; + + text: "LOG IN" + + onClicked: { + sendToScript({method: 'needsLogIn_loginClicked'}); + } + } + + RalewayRegular { + id: itemsLoginText + + anchors { + leftMargin: 15 + top: parent.top; + bottom: parent.bottom; + right: parent.right; + left: loginButton.right + } + + text: "to get items from the Marketplace." + color: hifi.colors.baseGray + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 18 + } + } + + Item { + id: breadcrumbs + + anchors.top: itemsLoginStatus.bottom; + anchors.left: parent.left + anchors.right: parent.right + height: 34 + visible: categoriesListView.currentIndex >= 0 + + RalewayRegular { + id: categoriesItemText + + anchors.leftMargin: 15 + anchors.fill:parent + + text: "Home /" + color: hifi.colors.blueHighlight + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 18 + } + + MouseArea { + anchors.fill: parent + + onClicked: { + categoriesListView.currentIndex = -1; + categoriesText.text = "Categories"; + root.categoryString = ""; + searchField.text = ""; + getMarketplaceItems(); + } + } + } + + Item { + id: searchScope + + anchors { + top: breadcrumbs.bottom + left: parent.left + right: parent.right + } + height: 50 + + RalewaySemiBold { + id: searchScopeText; + + anchors { + leftMargin: 15 + fill:parent + topMargin: 10 + } + + text: searchScopeString + color: hifi.colors.baseGray + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 22 + } + } + Item { + id: sort + visible: searchString === undefined || searchString === "" + + anchors { + top: searchScope.bottom; + left: parent.left; + right: parent.right; + topMargin: 10; + leftMargin: 15; + } + height: visible ? childrenRect.height : 0 + + RalewayRegular { + id: sortText + + anchors.leftMargin: 15 + anchors.rightMargin: 20 + height: 34 + + text: "Sort:" + color: hifi.colors.lightGrayText + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 14 + } + + Rectangle { + anchors { + left: sortText.right + top: parent.top + leftMargin: 20 + } + width: root.isLoggedIn ? 322 : 242 + height: 36 + + radius: 4 + border.width: 1 + border.color: hifi.colors.faintGray + + ListModel { + id: sortModel + + ListElement { + name: "Name"; + glyph: ";" + sortString: "alpha" + } + + ListElement { + name: "Date"; + glyph: ";"; + sortString: "recent"; + } + + ListElement { + name: "Popular"; + glyph: ";"; + sortString: "likes"; + } + + ListElement { + name: "My Likes"; + glyph: ";"; + sortString: "my_likes"; + } + } + + ListView { + id: sortListView + + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + topMargin:1 + bottomMargin:1 + leftMargin:1 + rightMargin:1 + } + width: childrenRect.width + height: 34 + + orientation: ListView.Horizontal + focus: true + clip: true + highlightFollowsCurrentItem: false + currentIndex: 1; + + delegate: SortButton { + width: 80 + height: parent.height + + glyph: model.glyph + text: model.name + + visible: root.isLoggedIn || model.sortString != "my_likes" + + checked: ListView.isCurrentItem + + onClicked: { + root.currentSortIndex = index; + sortListView.positionViewAtIndex(index, ListView.Beginning); + sortListView.currentIndex = index; + root.sortString = model.sortString; + getMarketplaceItems(); + } + } + highlight: Rectangle { + width: 80 + height: parent.height + + color: hifi.colors.faintGray + x: sortListView.currentItem.x + } + + model: sortModel + } + } + } + } + } + } + + // + // ITEMS LIST END + // + + // + // ITEM START + // + Item { + id: marketplaceItemView + + anchors.fill: parent + anchors.topMargin: 115 + anchors.bottomMargin: 50 + width: parent.width + + visible: false + + ScrollView { + id: marketplaceItemScrollView + + anchors.fill: parent + + clip: true + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + contentWidth: parent.width + contentHeight: childrenRect.height + + function resize() { + contentHeight = (marketplaceItemContent.y - itemSpacer.y + marketplaceItemContent.height); + } + + Item { + id: itemSpacer + anchors.top: parent.top + height: 15 + } + + Rectangle { + id: itemLoginStatus; + anchors { + left: parent.left + right: parent.right + top: itemSpacer.bottom + topMargin: 10 + leftMargin: 15 + rightMargin: 15 + } + height: root.isLoggedIn ? 0 : 80 + + visible: !root.isLoggedIn + color: hifi.colors.greenHighlight + border.color: hifi.colors.greenShadow + border.width: 1 + radius: 4 + z: 10000 + + HifiControlsUit.Button { + id: loginButton; + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + leftMargin: 15 + topMargin:10 + bottomMargin: 10 + } + width: 80; + + text: "LOG IN" + + onClicked: { + sendToScript({method: 'needsLogIn_loginClicked'}); + } + } + + RalewayRegular { + id: itemsLoginText + + anchors { + leftMargin: 15 + top: parent.top; + bottom: parent.bottom; + right: parent.right; + left: loginButton.right + } + + text: "to get items from the Marketplace." + color: hifi.colors.baseGray + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 18 + } + } + + + Rectangle { + id: marketplaceItemContent + anchors.top: itemLoginStatus.bottom + width: parent.width + height: childrenRect.height; + + RalewaySemiBold { + id: backText + + anchors { + top: parent.top + left: parent.left + topMargin: 10 + leftMargin: 15 + bottomMargin: 10 + } + width: paintedWidth + height: paintedHeight + + text: "Back" + size: hifi.fontSizes.overlayTitle + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter + } + + MouseArea { + anchors.fill: backText + + onClicked: { + getMarketplaceItems(); + } + } + + MarketplaceItem { + id: marketplaceItem + + + anchors.topMargin: 15 + anchors.top: backText.bottom + width: parent.width + height: childrenRect.height + + isLoggedIn: root.isLoggedIn; + + onBuy: { + sendToScript({method: 'marketplace_checkout', itemId: item_id, itemEdition: edition}); + } + + onShowLicense: { + var xhr = new XMLHttpRequest; + xhr.open("GET", url); + xhr.onreadystatechange = function() { + if (xhr.readyState == XMLHttpRequest.DONE) { + console.log(xhr.responseText); + licenseText.text = xhr.responseText; + licenseInfo.visible = true; + } + }; + xhr.send(); + } + onCategoryClicked: { + root.categoryString = category; + categoriesText.text = category; + getMarketplaceItems(); + } + + onResized: { + marketplaceItemScrollView.resize(); + } + } + } + } + } + // + // ITEM END + // + + // + // FOOTER START + // + + Rectangle { + id: footer + + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + height: 50 + + color: hifi.colors.blueHighlight + + Item { + anchors { + fill: parent + rightMargin: 15 + leftMargin: 15 + } + + + + Item { + id: footerText + + anchors.fill: parent + visible: root.supports3DHTML && itemsList.visible + + HiFiGlyphs { + id: footerGlyph + + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + rightMargin: 10 + } + + text: hifi.glyphs.info + size: 34 + color: hifi.colors.white + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: footerInfo + + anchors { + left: footerGlyph.right + top: parent.top + bottom: parent.bottom + } + + text: "Get items from Clara.io!" + color: hifi.colors.white + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + size: 18 + } + } + + HifiControlsUit.Button { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + topMargin: 10 + bottomMargin: 10 + leftMargin: 10 + rightMargin: 10 + } + + visible: marketplaceItemView.visible + text: "< BACK" + width: 100 + + onClicked: { + marketplaceItemView.visible = false; + itemsList.visible = true; + licenseInfo.visible = false; + } + } + + HifiControlsUit.Button { + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + topMargin: 10 + bottomMargin: 10 + leftMargin: 10 + rightMargin: 10 + } + + visible: root.supports3DHTML + + text: "SEE ALL MARKETS" + width: 180 + + onClicked: { + sendToScript({method: 'marketplace_marketplaces', itemId: marketplaceItemView.visible ? marketplaceItem.item_id : undefined}); + } + } + } + } + // + // FOOTER END + // + + + // + // LICENSE START + // + Rectangle { + id: licenseInfo + + anchors { + fill: root + topMargin: 120 + bottomMargin: 0 + } + + visible: false; + + ScrollView { + anchors { + bottomMargin: 1 + topMargin: 60 + leftMargin: 15 + fill: parent + } + + RalewayRegular { + id: licenseText + + width:440 + wrapMode: Text.Wrap + + text: "" + size: 18; + color: hifi.colors.baseGray + } + } + + Item { + id: licenseClose + + anchors { + top: parent.top + right: parent.right + topMargin: 10 + rightMargin: 10 + } + width: 30 + height: 30 + + HiFiGlyphs { + anchors { + fill: parent + rightMargin: 0 + verticalCenter: parent.verticalCenter + } + height: 30 + + text: hifi.glyphs.close + size: 34 + horizontalAlignment: Text.AlignHCenter + color: hifi.colors.baseGray + } + + MouseArea { + anchors.fill: licenseClose + + onClicked: licenseInfo.visible = false; + } + } + } + // + // LICENSE END + // + + HifiControlsUit.Keyboard { + id: keyboard + + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: false + } + + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript, in this case the Marketplaces JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + + function fromScript(message) { + switch (message.method) { + case 'updateMarketplaceQMLItem': + if (!message.params.itemId) { + console.log("A message with method 'updateMarketplaceQMLItem' was sent without an itemId!"); + return; + } + marketplaceItem.edition = message.params.edition ? message.params.edition : -1; + MarketplaceScriptingInterface.getMarketplaceItem(message.params.itemId); + break; + } + } +} diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml new file mode 100644 index 0000000000..24ef528673 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -0,0 +1,648 @@ +// +// MarketplaceListItem.qml +// qml/hifi/commerce/marketplace +// +// MarketplaceListItem +// +// Created by Roxanne Skelly on 2019-01-22 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import QtWebEngine 1.5 +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "../common/sendAsset" +import "../.." as HifiCommon + +Rectangle { + id: root; + + property string item_id: "" + property string image_url: "" + property string name: "" + property int likes: 0 + property bool liked: false + property string creator: "" + property var categories: [] + property int price: 0 + property var attributions: [] + property string description: "" + property string license: "" + property string posted: "" + property bool available: false + property string created_at: "" + property bool isLoggedIn: false; + property int edition: -1; + property bool supports3DHTML: false; + + + onCategoriesChanged: { + categoriesListModel.clear(); + categories.forEach(function(category) { + categoriesListModel.append({"category":category}); + }); + } + + onDescriptionChanged: { + + if(root.supports3DHTML) { + descriptionTextModel.clear(); + descriptionTextModel.append({text: description}); + } else { + descriptionText.text = description; + } + } + + onAttributionsChanged: { + attributionsModel.clear(); + if(root.attributions) { + root.attributions.forEach(function(attribution) { + attributionsModel.append(attribution); + }); + } + footer.evalHeight(); + } + + signal buy() + signal categoryClicked(string category) + signal showLicense(string url) + + HifiConstants { + id: hifi + } + + Connections { + target: MarketplaceScriptingInterface + + onMarketplaceItemLikeResult: { + if (result.status !== 'success') { + console.log("Like/Unlike item", result.data.message); + } else { + root.liked = !root.liked; + root.likes = root.liked ? root.likes + 1 : root.likes - 1; + } + } + } + function getFormattedDate(timestamp) { + function addLeadingZero(n) { + return n < 10 ? '0' + n : '' + n; + } + + var a = new Date(timestamp); + + var year = a.getFullYear(); + var month = addLeadingZero(a.getMonth() + 1); + var day = addLeadingZero(a.getDate()); + var hour = a.getHours(); + var drawnHour = hour; + if (hour === 0) { + drawnHour = 12; + } else if (hour > 12) { + drawnHour -= 12; + } + drawnHour = addLeadingZero(drawnHour); + + var amOrPm = "AM"; + if (hour >= 12) { + amOrPm = "PM"; + } + + var min = addLeadingZero(a.getMinutes()); + var sec = addLeadingZero(a.getSeconds()); + return a.toDateString() + " " + drawnHour + ':' + min + amOrPm; + } + function evalHeight() { + height = footer.y - header.y + footer.height; + } + + signal resized() + + onHeightChanged: { + resized(); + } + + anchors { + left: parent.left + right: parent.right + leftMargin: 15 + rightMargin: 15 + } + height: footer.y - header.y + footer.height + + Rectangle { + id: header + + anchors { + left: parent.left + right: parent.right + top: parent.top + } + height: 50 + + RalewaySemiBold { + id: nameText + + anchors { + top: parent.top + left: parent.left + bottom: parent.bottom + } + width: paintedWidth + + text: root.name + size: 24 + color: hifi.colors.baseGray + verticalAlignment: Text.AlignVCenter + } + + Item { + id: likes + + anchors { + top: parent.top + right: parent.right + bottom: parent.bottom + rightMargin: 5 + } + + RalewaySemiBold { + id: heart + + anchors { + top: parent.top + right: parent.right + rightMargin: 0 + verticalCenter: parent.verticalCenter + } + + text: "\u2665" + size: 20 + horizontalAlignment: Text.AlignHCenter + color: root.liked ? hifi.colors.redAccent : hifi.colors.lightGrayText + } + + RalewaySemiBold { + id: likesText + + anchors { + top: parent.top + right: heart.left + rightMargin: 5 + leftMargin: 15 + bottom: parent.bottom + } + width: paintedWidth + + text: root.likes + size: hifi.fontSizes.overlayTitle + color: hifi.colors.baseGray + verticalAlignment: Text.AlignVCenter + } + + MouseArea { + anchors { + left: likesText.left + right: heart.right + top: likesText.top + bottom: likesText.bottom + } + + onClicked: { + if (isLoggedIn) { + MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, !root.liked); + } + } + } + } + } + + Image { + id: itemImage + + anchors { + top: header.bottom + left: parent.left + right: parent.right + } + height: width*0.5625 + + source: root.image_url + fillMode: Image.PreserveAspectCrop; + } + + Item { + id: footer + + anchors { + left: parent.left; + right: parent.right; + top: itemImage.bottom; + } + height: categoriesList.y - buyButton.y + categoriesList.height + + function evalHeight() { + height = categoriesList.y - buyButton.y + categoriesList.height; + console.log("HEIGHT: " + height); + } + + HifiControlsUit.Button { + id: buyButton + + anchors { + right: parent.right + top: parent.top + left: parent.left + topMargin: 15 + } + height: 50 + + text: root.edition >= 0 ? "UPGRADE FOR FREE" : (root.available ? (root.price ? root.price : "FREE") : "UNAVAILABLE (not for sale)") + enabled: root.edition >= 0 || root.available + buttonGlyph: root.available ? (root.price ? hifi.glyphs.hfc : "") : "" + color: hifi.buttons.blue + + onClicked: root.buy(); + } + + Item { + id: creatorItem + + anchors { + top: buyButton.bottom + leftMargin: 15 + topMargin: 15 + } + width: parent.width + height: childrenRect.height + + RalewaySemiBold { + id: creatorLabel + + anchors.top: parent.top + anchors.left: parent.left + width: paintedWidth + + text: "CREATOR:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: creatorText + + anchors { + top: creatorLabel.bottom + left: parent.left + topMargin: 10 + } + width: paintedWidth + text: root.creator + + size: 18 + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter + } + } + + Item { + id: posted + + anchors { + top: creatorItem.bottom + leftMargin: 15 + topMargin: 15 + } + width: parent.width + height: childrenRect.height + + RalewaySemiBold { + id: postedLabel + + anchors.top: parent.top + anchors.left: parent.left + width: paintedWidth + + text: "POSTED:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: created_at + + anchors { + top: postedLabel.bottom + left: parent.left + right: parent.right + topMargin: 5 + } + + text: { getFormattedDate(root.created_at); } + size: 14 + color: hifi.colors.lightGray + verticalAlignment: Text.AlignVCenter + } + } + + Rectangle { + + anchors { + top: posted.bottom + leftMargin: 15 + topMargin: 15 + } + width: parent.width + height: 1 + + color: hifi.colors.lightGrayText + } + + Item { + id: attributions + + anchors { + top: posted.bottom + topMargin: 30 + left: parent.left + right: parent.right + } + width: parent.width + height: attributionsModel.count > 0 ? childrenRect.height : 0 + visible: attributionsModel.count > 0 + + RalewaySemiBold { + id: attributionsLabel + + anchors.top: parent.top + anchors.left: parent.left + width: paintedWidth + height: paintedHeight + + text: "ATTRIBUTIONS:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter + } + ListModel { + id: attributionsModel + } + + ListView { + anchors { + left: parent.left + right: parent.right + top: attributionsLabel.bottom + bottomMargin: 15 + } + + height: 24*model.count+10 + + model: attributionsModel + delegate: Item { + height: 24 + width: parent.width + RalewaySemiBold { + id: attributionName + + anchors.leftMargin: 15 + height: 24 + width: parent.width + + text: model.name + elide: Text.ElideRight + size: 14 + + color: model.link ? hifi.colors.blueHighlight : hifi.colors.baseGray + verticalAlignment: Text.AlignVCenter + MouseArea { + anchors.fill: parent + + onClicked: { + if (model.link) { + sendToScript({method: 'marketplace_open_link', link: model.link}); + } + } + } + } + } + } + Rectangle { + + anchors { + bottom: attributions.bottom + leftMargin: 15 + } + width: parent.width + height: 1 + + color: hifi.colors.lightGrayText + } + } + Item { + id: licenseItem; + + anchors { + top: attributions.bottom + left: parent.left + topMargin: 15 + } + width: parent.width + height: childrenRect.height + + RalewaySemiBold { + id: licenseLabel + + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + + text: "SHARED UNDER:"; + size: 14; + color: hifi.colors.lightGrayText; + verticalAlignment: Text.AlignVCenter; + } + + RalewaySemiBold { + id: licenseText + + anchors.top: licenseLabel.bottom + anchors.left: parent.left + anchors.topMargin: 5 + width: paintedWidth + + text: root.license + size: 14 + color: hifi.colors.lightGray + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: licenseHelp + + anchors.top: licenseText.bottom + anchors.left: parent.left + anchors.topMargin: 5 + width: paintedWidth + + text: "More about this license" + size: 14 + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter + + MouseArea { + anchors.fill: parent + + onClicked: { + licenseInfo.visible = true; + var url; + if (root.license === "No Rights Reserved (CC0)") { + url = "https://creativecommons.org/publicdomain/zero/1.0/" + } else if (root.license === "Attribution (CC BY)") { + url = "https://creativecommons.org/licenses/by/4.0/legalcode.txt" + } else if (root.license === "Attribution-ShareAlike (CC BY-SA)") { + url = "https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt" + } else if (root.license === "Attribution-NoDerivs (CC BY-ND)") { + url = "https://creativecommons.org/licenses/by-nd/4.0/legalcode.txt" + } else if (root.license === "Attribution-NonCommercial (CC BY-NC)") { + url = "https://creativecommons.org/licenses/by-nc/4.0/legalcode.txt" + } else if (root.license === "Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)") { + url = "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt" + } else if (root.license === "Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)") { + url = "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.txt" + } else if (root.license === "Proof of Provenance License (PoP License)") { + url = "https://digitalassetregistry.com/PoP-License/v1/" + } + if(url) { + showLicense(url) + } + } + } + } + } + + Item { + id: descriptionItem + property string text: "" + + anchors { + top: licenseItem.bottom + topMargin: 15 + left: parent.left + right: parent.right + } + height: childrenRect.height + onHeightChanged: { + footer.evalHeight(); + } + RalewaySemiBold { + id: descriptionLabel + + anchors.top: parent.top + anchors.left: parent.left + width: paintedWidth + height: 20 + + text: "DESCRIPTION:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: descriptionText + + anchors.top: descriptionLabel.bottom + anchors.left: parent.left + anchors.topMargin: 5 + width: parent.width + + text: root.description + size: 14 + color: hifi.colors.lightGray + linkColor: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter + textFormat: Text.RichText + wrapMode: Text.Wrap + onLinkActivated: { + sendToScript({method: 'marketplace_open_link', link: link}); + } + + onHeightChanged: { footer.evalHeight(); } + } + } + + Item { + id: categoriesList + + anchors { + top: descriptionItem.bottom + topMargin: 15 + left: parent.left + right: parent.right + } + width: parent.width + height: categoriesListModel.count*24 + categoryLabel.height + (isLoggedIn ? 50 : 150) + + onHeightChanged: { footer.evalHeight(); } + + RalewaySemiBold { + id: categoryLabel + + anchors.top: parent.top + anchors.left: parent.left + width: paintedWidth + + text: "IN:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter + } + ListModel { + id: categoriesListModel + } + + ListView { + anchors { + left: parent.left + right: parent.right + top: categoryLabel.bottom + bottomMargin: 15 + } + + height: 24*model.count+10 + + model: categoriesListModel + delegate: RalewaySemiBold { + id: categoryText + + anchors.leftMargin: 15 + width: paintedWidth + + text: model.category + size: 14 + height: 24 + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter + + MouseArea { + anchors.fill: categoryText + + onClicked: root.categoryClicked(model.category); + } + } + } + } + } +} diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml new file mode 100644 index 0000000000..2f37637e40 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml @@ -0,0 +1,321 @@ +// +// MarketplaceListItem.qml +// qml/hifi/commerce/marketplace +// +// MarketplaceListItem +// +// Created by Roxanne Skelly on 2019-01-22 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "../common/sendAsset" +import "../.." as HifiCommon + +Rectangle { + id: root + + property string item_id: "" + property string image_url: "" + property string name: "" + property int likes: 0 + property bool liked: false + property string creator: "" + property string category: "" + property int price: 0 + property bool available: false + property bool isLoggedIn: false; + + signal buy() + signal showItem() + signal categoryClicked(string category) + signal requestReload() + + HifiConstants { id: hifi } + + width: parent.width + height: childrenRect.height+50 + + DropShadow { + anchors.fill: shadowBase + + source: shadowBase + verticalOffset: 4 + horizontalOffset: 4 + radius: 6 + samples: 9 + color: hifi.colors.baseGrayShadow + } + + Rectangle { + id: shadowBase + + anchors.fill: itemRect + + color: "white" + } + + Rectangle { + id: itemRect + + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: 20 + bottomMargin: 10 + leftMargin: 15 + rightMargin: 15 + } + height: childrenRect.height + + MouseArea { + anchors.fill: parent + + hoverEnabled: true + + onClicked: root.showItem(); + onEntered: hoverRect.visible = true; + onExited: hoverRect.visible = false; + + } + + Rectangle { + id: header + + anchors { + left: parent.left + right: parent.right + top: parent.top + } + height: 50 + + RalewaySemiBold { + id: nameText + + anchors { + top: parent.top + left: parent.left + right: parent.right + rightMargin: 50 + leftMargin: 15 + bottom: parent.bottom + } + width: paintedWidth + + text: root.name + size: hifi.fontSizes.overlayTitle + elide: Text.ElideRight + color: hifi.colors.blueHighlight + verticalAlignment: Text.AlignVCenter + } + + Item { + id: likes + + anchors { + top: parent.top + right: parent.right + rightMargin: 15 + bottom: parent.bottom + } + width: heart.width + likesText.width + + Connections { + target: MarketplaceScriptingInterface + + onMarketplaceItemLikeResult: { + if (result.status !== 'success') { + console.log("Failed to get Marketplace Categories", result.data.message); + root.requestReload(); + } + } + } + + RalewaySemiBold { + id: heart + + anchors { + top: parent.top + right: parent.right + rightMargin: 0 + verticalCenter: parent.verticalCenter + } + + text: "\u2665" + size: 20 + horizontalAlignment: Text.AlignHCenter; + color: root.liked ? hifi.colors.redAccent : hifi.colors.lightGrayText + } + + RalewaySemiBold { + id: likesText + + anchors { + top: parent.top + right: heart.left + rightMargin: 5 + leftMargin: 15 + bottom: parent.bottom + } + width: paintedWidth + + text: root.likes + size: hifi.fontSizes.overlayTitle + color: hifi.colors.baseGray + verticalAlignment: Text.AlignVCenter + } + + MouseArea { + anchors { + left: likesText.left + right: heart.right + top: parent.top + bottom: parent.bottom + } + onClicked: { + if (isLoggedIn) { + root.liked = !root.liked; + root.likes = root.liked ? root.likes + 1 : root.likes - 1; + MarketplaceScriptingInterface.marketplaceItemLike(root.item_id, root.liked); + } + } + } + } + } + + Image { + id: itemImage + + anchors { + top: header.bottom + left: parent.left + right: parent.right + } + height: width * 0.5625 + + source: root.image_url + fillMode: Image.PreserveAspectCrop + } + + Item { + id: footer + + anchors { + left: parent.left + right: parent.right + top: itemImage.bottom + } + height: 60 + + RalewaySemiBold { + id: creatorLabel + + anchors { + top: parent.top + left: parent.left + leftMargin: 15 + } + width: paintedWidth + + text: "CREATOR:" + size: 14 + color: hifi.colors.lightGrayText + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: creatorText + + anchors { + top: creatorLabel.top; + left: creatorLabel.right; + leftMargin: 15; + } + width: paintedWidth; + + text: root.creator; + size: 14; + color: hifi.colors.lightGray; + verticalAlignment: Text.AlignVCenter; + } + + RalewaySemiBold { + id: categoryLabel + + anchors { + top: creatorLabel.bottom + left: parent.left + leftMargin: 15 + } + width: paintedWidth; + + text: "IN:"; + size: 14; + color: hifi.colors.lightGrayText; + verticalAlignment: Text.AlignVCenter; + } + + RalewaySemiBold { + id: categoryText + + anchors { + top: categoryLabel.top + left: categoryLabel.right + leftMargin: 15 + } + width: paintedWidth + + text: root.category + size: 14 + color: hifi.colors.blueHighlight; + verticalAlignment: Text.AlignVCenter; + + MouseArea { + anchors.fill: parent + + onClicked: root.categoryClicked(root.category); + } + } + + HifiControlsUit.Button { + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + rightMargin: 15 + topMargin:10 + bottomMargin: 10 + } + + text: root.price ? root.price : "FREE" + buttonGlyph: root.price ? hifi.glyphs.hfc : "" + color: hifi.buttons.blue; + + onClicked: root.buy(); + } + } + + Rectangle { + id: hoverRect + + anchors.fill: parent + + border.color: hifi.colors.blueHighlight + border.width: 2 + color: "#00000000" + visible: false + } + } +} diff --git a/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml b/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml new file mode 100644 index 0000000000..37ad2735ce --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplace/SortButton.qml @@ -0,0 +1,87 @@ +// +// SortButton.qml +// qml/hifi/commerce/marketplace +// +// SortButton +// +// Created by Roxanne Skelly on 2019-01-18 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 as Hifi +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../../../controls" as HifiControls +import "../common" as HifiCommerceCommon +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "../common/sendAsset" +import "../.." as HifiCommon + +Item { + HifiConstants { id: hifi; } + + id: root; + + + property string glyph: ""; + property string text: ""; + property bool checked: false; + signal clicked(); + + width: childrenRect.width; + height: parent.height; + + Rectangle { + anchors.top: parent.top; + anchors.left: parent.left; + height: parent.height; + width: 2; + color: hifi.colors.faintGray; + visible: index > 0; + } + + HiFiGlyphs { + id: buttonGlyph; + text: root.glyph; + // Size + size: 14; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 0; + anchors.top: parent.top; + anchors.verticalCenter: parent.verticalCenter; + height: parent.height; + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.lightGray; + } + RalewayRegular { + id: buttonText; + text: root.text; + // Text size + size: 14; + // Style + color: hifi.colors.lightGray; + elide: Text.ElideRight; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.top: parent.top; + height: parent.height; + } + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + root.clicked(); + } + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index f7dc26df5f..df6e216b32 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -28,6 +28,7 @@ Item { property string purchaseStatus; property string itemName; property string itemId; + property string updateItemId; property string itemPreviewImageUrl; property string itemHref; property string certificateId; @@ -45,9 +46,9 @@ Item { property bool cardBackVisible; property bool isInstalled; property string wornEntityID; - property string upgradeUrl; + property string updatedItemId; property string upgradeTitle; - property bool updateAvailable: root.upgradeUrl !== ""; + property bool updateAvailable: root.updateItemId && root.updateItemId !== ""; property bool valid; property string originalStatusText; @@ -175,7 +176,7 @@ Item { Item { property alias buttonGlyphText: buttonGlyph.text; - property alias buttonText: buttonText.text; + property alias itemButtonText: buttonText.text; property alias glyphSize: buttonGlyph.size; property string buttonColor: hifi.colors.black; property string buttonColor_hover: hifi.colors.blueHighlight; @@ -243,7 +244,7 @@ Item { onLoaded: { item.enabled = root.valid; item.buttonGlyphText = hifi.glyphs.gift; - item.buttonText = "Gift"; + item.itemButtonText = "Gift"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); sendToPurchases({ @@ -270,7 +271,7 @@ Item { onLoaded: { item.buttonGlyphText = hifi.glyphs.market; - item.buttonText = "View in Marketplace"; + item.itemButtonText = "View in Marketplace"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); sendToPurchases({method: 'purchases_itemInfoClicked', itemId: root.itemId}); @@ -288,7 +289,7 @@ Item { onLoaded: { item.buttonGlyphText = hifi.glyphs.certificate; - item.buttonText = "View Certificate"; + item.itemButtonText = "View Certificate"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); sendToPurchases({method: 'purchases_itemCertificateClicked', itemCertificateId: root.certificateId}); @@ -307,7 +308,7 @@ Item { onLoaded: { item.buttonGlyphText = hifi.glyphs.uninstall; - item.buttonText = "Uninstall"; + item.itemButtonText = "Uninstall"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); Commerce.uninstallApp(root.itemHref); @@ -330,15 +331,14 @@ Item { onLoaded: { item.buttonGlyphText = hifi.glyphs.update; - item.buttonText = "Update"; + item.itemButtonText = "Update"; item.buttonColor = "#E2334D"; item.buttonClicked = function() { sendToPurchases({ method: 'flipCard', closeAll: true }); sendToPurchases({ method: 'updateItemClicked', - itemId: root.itemId, + itemId: root.updateAvailable ? root.updateItemId : root.itemId, itemEdition: root.itemEdition, - upgradeUrl: root.upgradeUrl, itemHref: root.itemHref, itemType: root.itemType, isInstalled: root.isInstalled, @@ -378,10 +378,10 @@ Item { function updateProperties() { if (updateButton.visible && uninstallButton.visible) { - item.buttonText = ""; + item.itemButtonText = ""; item.glyphSize = 20; } else { - item.buttonText = "Send to Trash"; + item.itemButtonText = "Send to Trash"; item.glyphSize = 30; } } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 9433618b6b..bcc2a2821c 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -29,7 +29,6 @@ Rectangle { id: root; property string activeView: "initialize"; - property string referrerURL: ""; property bool securityImageResultReceived: false; property bool purchasesReceived: false; property bool punctuationMode: false; @@ -154,55 +153,10 @@ Rectangle { } } - // - // TITLE BAR START - // - HifiCommerceCommon.EmulatedMarketplaceHeader { - id: titleBarContainer; - z: 997; - visible: false; - height: 100; - // Size - width: parent.width; - // Anchors - anchors.left: parent.left; - anchors.top: parent.top; - - Connections { - onSendToParent: { - if (msg.method === 'needsLogIn' && root.activeView !== "needsLogIn") { - root.activeView = "needsLogIn"; - } else if (msg.method === 'showSecurityPicLightbox') { - lightboxPopup.titleText = "Your Security Pic"; - lightboxPopup.bodyImageSource = msg.securityImageSource; - lightboxPopup.bodyText = lightboxPopup.securityPicBodyText; - lightboxPopup.button1text = "CLOSE"; - lightboxPopup.button1method = function() { - lightboxPopup.visible = false; - } - lightboxPopup.visible = true; - } else { - sendToScript(msg); - } - } - } - } - MouseArea { - enabled: titleBarContainer.usernameDropdownVisible; - anchors.fill: parent; - onClicked: { - titleBarContainer.usernameDropdownVisible = false; - } - } - // - // TITLE BAR END - // - Rectangle { id: initialize; visible: root.activeView === "initialize"; - anchors.top: titleBarContainer.bottom; - anchors.topMargin: -titleBarContainer.additionalDropdownHeight; + anchors.top: parent.top; anchors.bottom: parent.bottom; anchors.left: parent.left; anchors.right: parent.right; @@ -219,8 +173,7 @@ Rectangle { id: installedAppsContainer; z: 998; visible: false; - anchors.top: titleBarContainer.bottom; - anchors.topMargin: -titleBarContainer.additionalDropdownHeight; + anchors.top: parent.top; anchors.left: parent.left; anchors.bottom: parent.bottom; width: parent.width; @@ -422,8 +375,8 @@ Rectangle { // Anchors anchors.left: parent.left; anchors.right: parent.right; - anchors.top: titleBarContainer.bottom; - anchors.topMargin: 8 - titleBarContainer.additionalDropdownHeight; + anchors.top: parent.top; + anchors.topMargin: 8; anchors.bottom: parent.bottom; // @@ -585,6 +538,7 @@ Rectangle { delegate: PurchasedItem { itemName: title; itemId: id; + updateItemId: model.upgrade_id ? model.upgrade_id : ""; itemPreviewImageUrl: preview; itemHref: download_url; certificateId: certificate_id; @@ -596,7 +550,6 @@ Rectangle { cardBackVisible: model.cardBackVisible || false; isInstalled: model.isInstalled || false; wornEntityID: model.wornEntityID; - upgradeUrl: model.upgrade_url; upgradeTitle: model.upgrade_title; itemType: model.item_type; valid: model.valid; @@ -1083,8 +1036,6 @@ Rectangle { function fromScript(message) { switch (message.method) { case 'updatePurchases': - referrerURL = message.referrerURL || ""; - titleBarContainer.referrerURL = message.referrerURL || ""; filterBar.text = message.filterText ? message.filterText : ""; break; case 'purchases_showMyItems': diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index cf293a06df..eb8aa0f809 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -335,7 +335,7 @@ Item { if (link.indexOf("users/") !== -1) { sendSignalToWallet({method: 'transactionHistory_usernameLinkClicked', usernameLink: link}); } else { - sendSignalToWallet({method: 'transactionHistory_linkClicked', marketplaceLink: link}); + sendSignalToWallet({method: 'transactionHistory_linkClicked', itemId: model.marketplace_item}); } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3b9fe15f25..98042470a6 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -235,6 +235,7 @@ #include "commerce/Ledger.h" #include "commerce/Wallet.h" #include "commerce/QmlCommerce.h" +#include "commerce/QmlMarketplace.h" #include "ResourceRequestObserver.h" #include "webbrowser/WebBrowserSuggestionsEngine.h" @@ -3008,7 +3009,23 @@ void Application::initializeUi() { QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, QUrl{ "hifi/tablet/TabletMenu.qml" }, + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, }, commerceCallback); + + QmlContextCallback marketplaceCallback = [](QQmlContext* context) { + context->setContextProperty("MarketplaceScriptingInterface", new QmlMarketplace()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, marketplaceCallback); + + QmlContextCallback platformInfoCallback = [](QQmlContext* context) { + context->setContextProperty("PlatformInfo", new PlatformInfoScriptingInterface()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, platformInfoCallback); + QmlContextCallback ttsCallback = [](QQmlContext* context) { context->setContextProperty("TextToSpeech", DependencyManager::get().data()); }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3f32f96795..b198edc245 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -75,7 +75,6 @@ const float PITCH_SPEED_DEFAULT = 75.0f; // degrees/sec const float MAX_BOOST_SPEED = 0.5f * DEFAULT_AVATAR_MAX_WALKING_SPEED; // action motor gets additive boost below this speed const float MIN_AVATAR_SPEED = 0.05f; -const float MIN_AVATAR_SPEED_SQUARED = MIN_AVATAR_SPEED * MIN_AVATAR_SPEED; // speed is set to zero below this float MIN_SCRIPTED_MOTOR_TIMESCALE = 0.005f; float DEFAULT_SCRIPTED_MOTOR_TIMESCALE = 1.0e6f; @@ -847,6 +846,7 @@ void MyAvatar::simulate(float deltaTime, bool inView) { updateOrientation(deltaTime); updatePosition(deltaTime); + updateViewBoom(); } // update sensorToWorldMatrix for camera and hand controllers @@ -3323,21 +3323,22 @@ void MyAvatar::updateActionMotor(float deltaTime) { direction = Vectors::ZERO; } + float sensorToWorldScale = getSensorToWorldScale(); if (state == CharacterController::State::Hover) { // we're flying --> complex acceleration curve that builds on top of current motor speed and caps at some max speed float motorSpeed = glm::length(_actionMotorVelocity); - float finalMaxMotorSpeed = getSensorToWorldScale() * DEFAULT_AVATAR_MAX_FLYING_SPEED * _walkSpeedScalar; + float finalMaxMotorSpeed = sensorToWorldScale * DEFAULT_AVATAR_MAX_FLYING_SPEED * _walkSpeedScalar; float speedGrowthTimescale = 2.0f; float speedIncreaseFactor = 1.8f * _walkSpeedScalar; motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale, 0.0f, 1.0f) * speedIncreaseFactor; - const float maxBoostSpeed = getSensorToWorldScale() * MAX_BOOST_SPEED; + const float maxBoostSpeed = sensorToWorldScale * MAX_BOOST_SPEED; if (_isPushing) { if (motorSpeed < maxBoostSpeed) { // an active action motor should never be slower than this float boostCoefficient = (maxBoostSpeed - motorSpeed) / maxBoostSpeed; - motorSpeed += getSensorToWorldScale() * MIN_AVATAR_SPEED * boostCoefficient; + motorSpeed += sensorToWorldScale * MIN_AVATAR_SPEED * boostCoefficient; } else if (motorSpeed > finalMaxMotorSpeed) { motorSpeed = finalMaxMotorSpeed; } @@ -3348,45 +3349,21 @@ void MyAvatar::updateActionMotor(float deltaTime) { const glm::vec2 currentVel = { direction.x, direction.z }; float scaledSpeed = scaleSpeedByDirection(currentVel, _walkSpeed.get(), _walkBackwardSpeed.get()); // _walkSpeedScalar is a multiplier if we are in sprint mode, otherwise 1.0 - _actionMotorVelocity = getSensorToWorldScale() * (scaledSpeed * _walkSpeedScalar) * direction; - } - - float previousBoomLength = _boomLength; - float boomChange = getDriveKey(ZOOM); - _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; - _boomLength = glm::clamp(_boomLength, ZOOM_MIN, ZOOM_MAX); - - // May need to change view if boom length has changed - if (previousBoomLength != _boomLength) { - qApp->changeViewAsNeeded(_boomLength); + _actionMotorVelocity = sensorToWorldScale * (scaledSpeed * _walkSpeedScalar) * direction; } } void MyAvatar::updatePosition(float deltaTime) { - if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { - updateActionMotor(deltaTime); - } - - vec3 velocity = getWorldVelocity(); - float sensorToWorldScale = getSensorToWorldScale(); - float sensorToWorldScale2 = sensorToWorldScale * sensorToWorldScale; - const float MOVING_SPEED_THRESHOLD_SQUARED = 0.0001f; // 0.01 m/s - if (!_characterController.isEnabledAndReady()) { - // _characterController is not in physics simulation but it can still compute its target velocity - updateMotors(); - _characterController.computeNewVelocity(deltaTime, velocity); - - float speed2 = glm::length(velocity); - if (speed2 > sensorToWorldScale2 * MIN_AVATAR_SPEED_SQUARED) { - // update position ourselves - applyPositionDelta(deltaTime * velocity); + if (_characterController.isEnabledAndReady()) { + if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { + updateActionMotor(deltaTime); } - measureMotionDerivatives(deltaTime); - _moving = speed2 > sensorToWorldScale2 * MOVING_SPEED_THRESHOLD_SQUARED; - } else { + float sensorToWorldScale = getSensorToWorldScale(); + float sensorToWorldScale2 = sensorToWorldScale * sensorToWorldScale; + vec3 velocity = getWorldVelocity(); float speed2 = glm::length2(velocity); + const float MOVING_SPEED_THRESHOLD_SQUARED = 0.0001f; // 0.01 m/s _moving = speed2 > sensorToWorldScale2 * MOVING_SPEED_THRESHOLD_SQUARED; - if (_moving) { // scan for walkability glm::vec3 position = getWorldPosition(); @@ -3398,6 +3375,18 @@ void MyAvatar::updatePosition(float deltaTime) { } } +void MyAvatar::updateViewBoom() { + float previousBoomLength = _boomLength; + float boomChange = getDriveKey(ZOOM); + _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; + _boomLength = glm::clamp(_boomLength, ZOOM_MIN, ZOOM_MAX); + + // May need to change view if boom length has changed + if (previousBoomLength != _boomLength) { + qApp->changeViewAsNeeded(_boomLength); + } +} + void MyAvatar::updateCollisionSound(const glm::vec3 &penetration, float deltaTime, float frequency) { // COLLISION SOUND API in Audio has been removed } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 0d27988543..c53eae65d4 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1732,6 +1732,7 @@ private: void updateOrientation(float deltaTime); void updateActionMotor(float deltaTime); void updatePosition(float deltaTime); + void updateViewBoom(); void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void initHeadBones(); void initAnimGraph(); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 5236c5a7fb..046e697b6d 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -54,11 +54,10 @@ void QmlCommerce::openSystemApp(const QString& appName) { {"GOTO", "hifi/tablet/TabletAddressDialog.qml"}, {"PEOPLE", "hifi/Pal.qml"}, {"WALLET", "hifi/commerce/wallet/Wallet.qml"}, - {"MARKET", "/marketplace.html"} + {"MARKET", "hifi/commerce/marketplace/Marketplace.qml"} }; static const QMap systemInject{ - {"MARKET", "/scripts/system/html/js/marketplacesInject.js"} }; diff --git a/interface/src/commerce/QmlMarketplace.cpp b/interface/src/commerce/QmlMarketplace.cpp new file mode 100644 index 0000000000..07a9e570bd --- /dev/null +++ b/interface/src/commerce/QmlMarketplace.cpp @@ -0,0 +1,135 @@ +// +// QmlMarketplace.cpp +// interface/src/commerce +// +// Guard for safe use of Marketplace by authorized QML. +// +// Created by Roxanne Skelly on 1/18/19. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#include "QmlMarketplace.h" +#include "CommerceLogging.h" +#include "Application.h" +#include "DependencyManager.h" +#include +#include +#include +#include +#include "scripting/HMDScriptingInterface.h" + +#define ApiHandler(NAME) void QmlMarketplace::NAME##Success(QNetworkReply* reply) { emit NAME##Result(apiResponse(#NAME, reply)); } +#define FailHandler(NAME) void QmlMarketplace::NAME##Failure(QNetworkReply* reply) { emit NAME##Result(failResponse(#NAME, reply)); } +#define Handler(NAME) ApiHandler(NAME) FailHandler(NAME) +Handler(getMarketplaceItems) +Handler(getMarketplaceItem) +Handler(marketplaceItemLike) +Handler(getMarketplaceCategories) + +QmlMarketplace::QmlMarketplace() { +} + +void QmlMarketplace::openMarketplace(const QString& marketplaceItemId) { + auto tablet = dynamic_cast( + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + tablet->loadQMLSource("hifi/commerce/marketplace/Marketplace.qml"); + DependencyManager::get()->openTablet(); + if (!marketplaceItemId.isEmpty()) { + tablet->sendToQml(QVariantMap({ { "method", "marketplace_openItem" }, { "itemId", marketplaceItemId } })); + } +} + +void QmlMarketplace::getMarketplaceItems( + const QString& q, + const QString& view, + const QString& category, + const QString& adminFilter, + const QString& adminFilterCost, + const QString& sort, + const bool isFree, + const int& page, + const int& perPage) { + + QString endpoint = "items"; + QUrlQuery request; + request.addQueryItem("q", q); + request.addQueryItem("view", view); + request.addQueryItem("category", category); + request.addQueryItem("adminFilter", adminFilter); + request.addQueryItem("adminFilterCost", adminFilterCost); + request.addQueryItem("sort", sort); + if (isFree) { + request.addQueryItem("isFree", "true"); + } + request.addQueryItem("page", QString::number(page)); + request.addQueryItem("perPage", QString::number(perPage)); + send(endpoint, "getMarketplaceItemsSuccess", "getMarketplaceItemsFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional, request); +} + +void QmlMarketplace::getMarketplaceItem(const QString& marketplaceItemId) { + QString endpoint = QString("items/") + marketplaceItemId; + QUrlQuery request; + send(endpoint, "getMarketplaceItemSuccess", "getMarketplaceItemFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional, request); +} + +void QmlMarketplace::marketplaceItemLike(const QString& marketplaceItemId, const bool like) { + QString endpoint = QString("items/") + marketplaceItemId + "/like"; + QUrlQuery request; + send(endpoint, "marketplaceItemLikeSuccess", "marketplaceItemLikeFailure", like ? QNetworkAccessManager::PostOperation : QNetworkAccessManager::DeleteOperation, AccountManagerAuth::Required, request); +} + +void QmlMarketplace::getMarketplaceCategories() { + QString endpoint = "categories"; + QUrlQuery request; + send(endpoint, "getMarketplaceCategoriesSuccess", "getMarketplaceCategoriesFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::None, request); +} + + +void QmlMarketplace::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, const QUrlQuery & request) { + auto accountManager = DependencyManager::get(); + const QString URL = "/api/v1/marketplace/"; + JSONCallbackParameters callbackParams(this, success, fail); + + accountManager->sendRequest(URL + endpoint, + authType, + method, + callbackParams, + QByteArray(), + NULL, + QVariantMap(), + request); + +} + +QJsonObject QmlMarketplace::apiResponse(const QString& label, QNetworkReply* reply) { + QByteArray response = reply->readAll(); + QJsonObject data = QJsonDocument::fromJson(response).object(); +#if defined(DEV_BUILD) // Don't expose user's personal data in the wild. But during development this can be handy. + qInfo(commerce) << label << "response" << QJsonDocument(data).toJson(QJsonDocument::Compact); +#endif + return data; +} + +// Non-200 responses are not json: +QJsonObject QmlMarketplace::failResponse(const QString& label, QNetworkReply* reply) { + QString response = reply->readAll(); + qWarning(commerce) << "FAILED" << label << response; + + // tempResult will be NULL if the response isn't valid JSON. + QJsonDocument tempResult = QJsonDocument::fromJson(response.toLocal8Bit()); + if (tempResult.isNull()) { + QJsonObject result + { + { "status", "fail" }, + { "message", response } + }; + return result; + } + else { + return tempResult.object(); + } +} \ No newline at end of file diff --git a/interface/src/commerce/QmlMarketplace.h b/interface/src/commerce/QmlMarketplace.h new file mode 100644 index 0000000000..f954198371 --- /dev/null +++ b/interface/src/commerce/QmlMarketplace.h @@ -0,0 +1,68 @@ +// +// QmlMarketplace.h +// interface/src/commerce +// +// Guard for safe use of Marketplace by authorized QML. +// +// Created by Roxanne Skelly on 1/18/19. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_QmlMarketplace_h +#define hifi_QmlMarketplace_h + +#include + +#include +#include +#include "AccountManager.h" + +class QmlMarketplace : public QObject { + Q_OBJECT + +public: + QmlMarketplace(); + +public slots: + void getMarketplaceItemsSuccess(QNetworkReply* reply); + void getMarketplaceItemsFailure(QNetworkReply* reply); + void getMarketplaceItemSuccess(QNetworkReply* reply); + void getMarketplaceItemFailure(QNetworkReply* reply); + void getMarketplaceCategoriesSuccess(QNetworkReply* reply); + void getMarketplaceCategoriesFailure(QNetworkReply* reply); + void marketplaceItemLikeSuccess(QNetworkReply* reply); + void marketplaceItemLikeFailure(QNetworkReply* reply); + +protected: + Q_INVOKABLE void openMarketplace(const QString& marketplaceItemId = QString()); + Q_INVOKABLE void getMarketplaceItems( + const QString& q = QString(), + const QString& view = QString(), + const QString& category = QString(), + const QString& adminFilter = QString("published"), + const QString& adminFilterCost = QString(), + const QString& sort = QString(), + const bool isFree = false, + const int& page = 1, + const int& perPage = 20); + Q_INVOKABLE void getMarketplaceItem(const QString& marketplaceItemId); + Q_INVOKABLE void marketplaceItemLike(const QString& marketplaceItemId, const bool like = true); + Q_INVOKABLE void getMarketplaceCategories(); + +signals: + void getMarketplaceItemsResult(QJsonObject result); + void getMarketplaceItemResult(QJsonObject result); + void getMarketplaceCategoriesResult(QJsonObject result); + void marketplaceItemLikeResult(QJsonObject result); + +private: + void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, const QUrlQuery & request); + QJsonObject apiResponse(const QString& label, QNetworkReply* reply); + QJsonObject failResponse(const QString& label, QNetworkReply* reply); +}; + +#endif // hifi_QmlMarketplace_h diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index b6e4df0d40..b390ab7119 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -133,3 +133,11 @@ bool PlatformInfoScriptingInterface::hasRiftControllers() { bool PlatformInfoScriptingInterface::hasViveControllers() { return qApp->hasViveControllers(); } + +bool PlatformInfoScriptingInterface::has3DHTML() { +#if defined(Q_OS_ANDROID) + return false; +#else + return true; +#endif +} diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h index 3ed57965c9..aece09b008 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.h +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -65,6 +65,12 @@ public slots: * @function Window.hasRift * @returns {boolean} true if running on Windows, otherwise false.*/ bool hasViveControllers(); + + /**jsdoc + * Returns true if device supports 3d HTML + * @function Window.hasRift + * @returns {boolean} true if device supports 3d HTML, otherwise false.*/ + bool has3DHTML(); }; #endif // hifi_PlatformInfoScriptingInterface_h diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index f74b337ee7..cf77c1cad5 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -208,7 +208,7 @@ void AccountManager::setSessionID(const QUuid& sessionID) { } } -QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType) { +QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType, const QUrlQuery & query) { QNetworkRequest networkRequest; networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); @@ -227,6 +227,7 @@ QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth:: } else { requestURL.setPath("/" + path); } + requestURL.setQuery(query); if (authType != AccountManagerAuth::None ) { if (hasValidAccessToken()) { @@ -263,13 +264,14 @@ void AccountManager::sendRequest(const QString& path, Q_ARG(const JSONCallbackParameters&, callbackParams), Q_ARG(const QByteArray&, dataByteArray), Q_ARG(QHttpMultiPart*, dataMultiPart), - Q_ARG(QVariantMap, propertyMap)); + Q_ARG(QVariantMap, propertyMap), + Q_ARG(QUrlQuery, query)); return; } QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = createRequest(path, authType); + QNetworkRequest networkRequest = createRequest(path, authType, query); if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qCDebug(networking) << "Making a request to" << qPrintable(networkRequest.url().toString()); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 477488031d..2ccebdd73c 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -61,7 +61,7 @@ class AccountManager : public QObject, public Dependency { public: AccountManager(UserAgentGetter userAgentGetter = DEFAULT_USER_AGENT_GETTER); - QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType); + QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType, const QUrlQuery & query = QUrlQuery()); Q_INVOKABLE void sendRequest(const QString& path, AccountManagerAuth::Type authType, QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp index a79f0a0c2b..8ab502e951 100755 --- a/libraries/octree/src/OctreePacketData.cpp +++ b/libraries/octree/src/OctreePacketData.cpp @@ -729,12 +729,6 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char *dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); - - // FIXME - this size check is wrong if we allow larger packets - if (length * sizeof(glm::vec3) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { - result.resize(0); - return sizeof(uint16_t); - } result.resize(length); memcpy(result.data(), dataBytes, length * sizeof(glm::vec3)); return sizeof(uint16_t) + length * sizeof(glm::vec3); @@ -744,14 +738,7 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char *dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); - - // FIXME - this size check is wrong if we allow larger packets - if (length * sizeof(glm::quat) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { - result.resize(0); - return sizeof(uint16_t); - } result.resize(length); - const unsigned char *start = dataBytes; for (int i = 0; i < length; i++) { dataBytes += unpackOrientationQuatFromBytes(dataBytes, result[i]); @@ -764,12 +751,6 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); - - // FIXME - this size check is wrong if we allow larger packets - if (length * sizeof(float) > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { - result.resize(0); - return sizeof(uint16_t); - } result.resize(length); memcpy(result.data(), dataBytes, length * sizeof(float)); return sizeof(uint16_t) + length * sizeof(float); @@ -779,14 +760,7 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto uint16_t length; memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); - - // FIXME - this size check is wrong if we allow larger packets - if (length / 8 > MAX_OCTREE_UNCOMRESSED_PACKET_SIZE) { - result.resize(0); - return sizeof(uint16_t); - } result.resize(length); - int bit = 0; unsigned char current = 0; const unsigned char *start = dataBytes; @@ -797,7 +771,6 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto result[i] = (bool)(current & (1 << bit)); bit = (bit + 1) % BITS_IN_BYTE; } - return (dataBytes - start) + (int)sizeof(uint16_t); } diff --git a/libraries/shared/src/CrashHelpers.cpp b/libraries/shared/src/CrashHelpers.cpp index f8ca90bc4c..1676318f3e 100644 --- a/libraries/shared/src/CrashHelpers.cpp +++ b/libraries/shared/src/CrashHelpers.cpp @@ -11,6 +11,16 @@ #include "CrashHelpers.h" +#ifdef NDEBUG +// undefine NDEBUG so doAssert() works for all builds +#undef NDEBUG +#include +#define NDEBUG +#else +#include +#endif + + namespace crash { class B; @@ -34,6 +44,11 @@ A::~A() { _b->virtualFunction(); } +// only use doAssert() for debug purposes +void doAssert(bool value) { + assert(value); +} + void pureVirtualCall() { qCDebug(shared) << "About to make a pure virtual call"; B b; diff --git a/libraries/shared/src/CrashHelpers.h b/libraries/shared/src/CrashHelpers.h index ad988c8906..247aea5cde 100644 --- a/libraries/shared/src/CrashHelpers.h +++ b/libraries/shared/src/CrashHelpers.h @@ -18,6 +18,7 @@ namespace crash { +void doAssert(bool value); // works for Release void pureVirtualCall(); void doubleFree(); void nullDeref(); diff --git a/libraries/shared/src/LogHandler.cpp b/libraries/shared/src/LogHandler.cpp index 65651373be..c51d9bf611 100644 --- a/libraries/shared/src/LogHandler.cpp +++ b/libraries/shared/src/LogHandler.cpp @@ -38,7 +38,6 @@ LogHandler::LogHandler() { } LogHandler::~LogHandler() { - flushRepeatedMessages(); } const char* stringForLogType(LogMsgType msgType) { diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 19efdc042c..17ff918243 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -379,21 +379,16 @@ function deleteSendMoneyParticleEffect() { function onUsernameChanged() { } -var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); -var METAVERSE_SERVER_URL = Account.metaverseServerURL; -var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. -function openMarketplace(optionalItemOrUrl) { - // This is a bit of a kluge, but so is the whole file. - // If given a whole path, use it with no cta. - // If given an id, build the appropriate url and use the id as the cta. - // Otherwise, use home and 'marketplace cta'. - // AND... if call onMarketplaceOpen to setupWallet if we need to. - var url = optionalItemOrUrl || MARKETPLACE_URL_INITIAL; - // If optionalItemOrUrl contains the metaverse base, then it's a url, not an item id. - if (optionalItemOrUrl && optionalItemOrUrl.indexOf(METAVERSE_SERVER_URL) === -1) { - url = MARKETPLACE_URL + '/items/' + optionalItemOrUrl; +var MARKETPLACE_QML_PATH = "hifi/commerce/marketplace/Marketplace.qml"; +function openMarketplace(optionalItem) { + ui.open(MARKETPLACE_QML_PATH); + + if (optionalItem) { + ui.tablet.sendToQml({ + method: 'updateMarketplaceQMLItem', + params: { itemId: optionalItem } + }); } - ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); } function setCertificateInfo(itemCertificateId) { @@ -425,10 +420,10 @@ function fromQml(message) { case 'purchases': case 'marketplace cta': case 'mainPage': - ui.open(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + openMarketplace(); break; - default: // User needs to return to an individual marketplace item URL - ui.open(MARKETPLACE_URL + '/items/' + message.referrer, MARKETPLACES_INJECT_SCRIPT_URL); + default: + openMarketplace(); break; } break; @@ -440,13 +435,13 @@ function fromQml(message) { case 'maybeEnableHmdPreview': break; // do nothing here, handled in marketplaces.js case 'transactionHistory_linkClicked': - ui.open(message.marketplaceLink, MARKETPLACES_INJECT_SCRIPT_URL); + openMarketplace(message.itemId); break; case 'goToMarketplaceMainPage': - ui.open(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + openMarketplace(); break; case 'goToMarketplaceItemPage': - ui.open(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); + openMarketplace(message.itemId); break; case 'refreshConnections': print('Refreshing Connections...'); diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index f1931192e4..8d408169ba 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -19,6 +19,7 @@ var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; var GOTO_DIRECTORY = "GOTO_DIRECTORY"; + var GOTO_MARKETPLACE = "GOTO_MARKETPLACE"; var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; @@ -72,7 +73,16 @@ // Footer actions. $("#back-button").on("click", function () { - (document.referrer !== "") ? window.history.back() : window.location = (marketplaceBaseURL + "/marketplace?"); + if (document.referrer !== "") { + window.history.back(); + } else { + var params = { type: GOTO_MARKETPLACE }; + var itemIdMatch = location.search.match(/itemId=([^&]*)/); + if (itemIdMatch && itemIdMatch.length === 2) { + params.itemId = itemIdMatch[1]; + } + EventBridge.emitWebEvent(JSON.stringify(params)); + } }); $("#all-markets").on("click", function () { EventBridge.emitWebEvent(JSON.stringify({ @@ -93,7 +103,9 @@ window.location = "https://clara.io/library?gameCheck=true&public=true"; }); $('#exploreHifiMarketplace').on('click', function () { - window.location = marketplaceBaseURL + "/marketplace?"; + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_MARKETPLACE + })); }); } diff --git a/scripts/system/marketplaces/marketplace.js b/scripts/system/marketplaces/marketplace.js index d3e5c96739..70680acd1d 100644 --- a/scripts/system/marketplaces/marketplace.js +++ b/scripts/system/marketplaces/marketplace.js @@ -1,8 +1,8 @@ // // marketplace.js // -// Created by Eric Levin on 8 Jan 2016 -// Copyright 2016 High Fidelity, Inc. +// Created by Roxanne Skelly on 1/18/2019 +// Copyright 2019 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -10,108 +10,27 @@ (function() { // BEGIN LOCAL_SCOPE -/* global WebTablet */ -Script.include("../libraries/WebTablet.js"); +var AppUi = Script.require('appUi'); -var toolIconUrl = Script.resolvePath("../assets/images/tools/"); +var BUTTON_NAME = "MARKET"; +var MARKETPLACE_QML_SOURCE = "hifi/commerce/marketplace/Marketplace.qml"; +var ui; +function startup() { -var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; -var marketplaceWindow = new OverlayWebWindow({ - title: "Marketplace", - source: "about:blank", - width: 900, - height: 700, - visible: false -}); - -var toolHeight = 50; -var toolWidth = 50; -var TOOLBAR_MARGIN_Y = 0; -var marketplaceVisible = false; -var marketplaceWebTablet; - -// We persist avatarEntity data in the .ini file, and reconsistitute it on restart. -// To keep things consistent, we pickle the tablet data in Settings, and kill any existing such on restart and domain change. -var persistenceKey = "io.highfidelity.lastDomainTablet"; - -function shouldShowWebTablet() { - var rightPose = Controller.getPoseValue(Controller.Standard.RightHand); - var leftPose = Controller.getPoseValue(Controller.Standard.LeftHand); - var hasHydra = !!Controller.Hardware.Hydra; - return HMD.active && (leftPose.valid || rightPose.valid || hasHydra); + ui = new AppUi({ + buttonName: BUTTON_NAME, + sortOrder: 10, + home: MARKETPLACE_QML_SOURCE + }); } -function showMarketplace(marketplaceID) { - var url = MARKETPLACE_URL; - if (marketplaceID) { - url = url + "/items/" + marketplaceID; - } - tablet.gotoWebScreen(url); - marketplaceVisible = true; - UserActivityLogger.openedMarketplace(); +function shutdown() { } -function hideTablet(tablet) { - if (!tablet) { - return; - } - updateButtonState(false); - tablet.unregister(); - tablet.destroy(); - marketplaceWebTablet = null; - Settings.setValue(persistenceKey, ""); -} -function clearOldTablet() { // If there was a tablet from previous domain or session, kill it and let it be recreated - -} -function hideMarketplace() { - if (marketplaceWindow.visible) { - marketplaceWindow.setVisible(false); - marketplaceWindow.setURL("about:blank"); - } else if (marketplaceWebTablet) { - - } - marketplaceVisible = false; -} - -function toggleMarketplace() { - if (marketplaceVisible) { - hideMarketplace(); - } else { - showMarketplace(); - } -} - -var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - -var browseExamplesButton = tablet.addButton({ - icon: "icons/tablet-icons/market-i.svg", - text: "MARKET" -}); - -function updateButtonState(visible) { - -} -function onMarketplaceWindowVisibilityChanged() { - updateButtonState(marketplaceWindow.visible); - marketplaceVisible = marketplaceWindow.visible; -} - -function onClick() { - toggleMarketplace(); -} - -browseExamplesButton.clicked.connect(onClick); -marketplaceWindow.visibleChanged.connect(onMarketplaceWindowVisibilityChanged); - -clearOldTablet(); // Run once at startup, in case there's anything laying around from a crash. -// We could also optionally do something like Window.domainChanged.connect(function () {Script.setTimeout(clearOldTablet, 2000)}), -// but the HUD version stays around, so lets do the same. - -Script.scriptEnding.connect(function () { - browseExamplesButton.clicked.disconnect(onClick); - tablet.removeButton(browseExamplesButton); - marketplaceWindow.visibleChanged.disconnect(onMarketplaceWindowVisibilityChanged); -}); +// +// Run the functions. +// +startup(); +Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index e4891a9bae..e059081741 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -19,12 +19,14 @@ var selectionDisplay = null; // for gridTool.js to ignore var AppUi = Script.require('appUi'); Script.include("/~/system/libraries/gridTool.js"); Script.include("/~/system/libraries/connectionUtils.js"); +Script.include("/~/system/libraries/accountUtils.js"); var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; var MARKETPLACE_ITEM_TESTER_QML_PATH = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"; var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; // HRS FIXME "hifi/commerce/purchases/Purchases.qml"; var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; +var MARKETPLACE_QML_PATH = "hifi/commerce/marketplace/Marketplace.qml"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var METAVERSE_SERVER_URL = Account.metaverseServerURL; @@ -36,6 +38,7 @@ var CLARA_IO_STATUS = "CLARA.IO STATUS"; var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; var GOTO_DIRECTORY = "GOTO_DIRECTORY"; +var GOTO_MARKETPLACE = "GOTO_MARKETPLACE"; var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; @@ -154,18 +157,14 @@ function onMarketplaceOpen(referrer) { } } -function openMarketplace(optionalItemOrUrl) { - // This is a bit of a kluge, but so is the whole file. - // If given a whole path, use it with no cta. - // If given an id, build the appropriate url and use the id as the cta. - // Otherwise, use home and 'marketplace cta'. - // AND... if call onMarketplaceOpen to setupWallet if we need to. - var url = optionalItemOrUrl || MARKETPLACE_URL_INITIAL; - // If optionalItemOrUrl contains the metaverse base, then it's a url, not an item id. - if (optionalItemOrUrl && optionalItemOrUrl.indexOf(METAVERSE_SERVER_URL) === -1) { - url = MARKETPLACE_URL + '/items/' + optionalItemOrUrl; +function openMarketplace(optionalItem, edition) { + ui.open(MARKETPLACE_QML_PATH); + if (optionalItem) { + ui.tablet.sendToQml({ + method: 'updateMarketplaceQMLItem', + params: { itemId: optionalItem, edition: edition } + }); } - ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); } // Function Name: wireQmlEventBridge() @@ -439,7 +438,9 @@ var referrerURL; // Used for updating Purchases QML var filterText; // Used for updating Purchases QML function onWebEventReceived(message) { message = JSON.parse(message); - if (message.type === GOTO_DIRECTORY) { + if (message.type === GOTO_MARKETPLACE) { + openMarketplace(message.itemId); + } else if (message.type === GOTO_DIRECTORY) { // This is the chooser between marketplaces. Only OUR markteplace // requires/makes-use-of wallet, so doesn't go through openMarketplace bottleneck. ui.open(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); @@ -485,7 +486,6 @@ function onWebEventReceived(message) { } else if (message.type === "WALLET_SETUP") { setupWallet('marketplace cta'); } else if (message.type === "MY_ITEMS") { - referrerURL = MARKETPLACE_URL_INITIAL; filterText = ""; ui.open(MARKETPLACE_PURCHASES_QML_PATH); wireQmlEventBridge(true); @@ -546,11 +546,10 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { openWallet(); break; case 'checkout_cancelClicked': - openMarketplace(message.params); + openMarketplace(message.itemId); break; case 'header_goToPurchases': case 'checkout_goToPurchases': - referrerURL = MARKETPLACE_URL_INITIAL; filterText = message.filterText; ui.open(MARKETPLACE_PURCHASES_QML_PATH); break; @@ -560,6 +559,25 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { case 'checkout_continue': openMarketplace(); break; + case 'marketplace_checkout': + ui.open(MARKETPLACE_CHECKOUT_QML_PATH); + ui.tablet.sendToQml({ + method: 'updateCheckoutQMLItemID', + params: message + }); + break; + case 'marketplace_marketplaces': + // This is the chooser between marketplaces. Only OUR markteplace + // requires/makes-use-of wallet, so doesn't go through openMarketplace bottleneck. + var url = MARKETPLACES_URL; + if(message.itemId) { + url = url + "?itemId=" + message.itemId + } + ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'marketplace_open_link': + ui.open(message.link); + break; case 'checkout_rezClicked': case 'purchases_rezClicked': case 'tester_rezClicked': @@ -588,18 +606,18 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { } break; case 'header_marketplaceImageClicked': - openMarketplace(message.referrerURL); + openMarketplace(); break; case 'purchases_goToMarketplaceClicked': openMarketplace(); break; case 'updateItemClicked': - openMarketplace(message.upgradeUrl + "?edition=" + message.itemEdition); + openMarketplace(message.itemId, message.itemEdition); break; case 'passphrasePopup_cancelClicked': case 'needsLogIn_cancelClicked': // Should/must NOT check for wallet setup. - ui.open(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + openMarketplace(); break; case 'needsLogIn_loginClicked': openLoginWindow(); @@ -624,10 +642,10 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { ContextOverlay.requestOwnershipVerification(message.entity); break; case 'inspectionCertificate_showInMarketplaceClicked': - openMarketplace(message.marketplaceUrl); + console.log("INSPECTION CERTIFICATE SHOW IN MARKETPLACE CLICKED: " + message.itemId); + openMarketplace(message.itemId); break; case 'header_myItemsClicked': - referrerURL = MARKETPLACE_URL_INITIAL; filterText = ""; ui.open(MARKETPLACE_PURCHASES_QML_PATH); wireQmlEventBridge(true); @@ -689,7 +707,7 @@ var onMarketplaceScreen = false; var onWalletScreen = false; var onTabletScreenChanged = function onTabletScreenChanged(type, url) { ui.setCurrentVisibleScreenMetadata(type, url); - onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; + onMarketplaceScreen = type === "QML" && url.indexOf(MARKETPLACE_QML_PATH) !== -1; onInspectionCertificateScreen = type === "QML" && url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1; var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; var onCommerceScreenNow = type === "QML" && ( @@ -699,7 +717,9 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { var onMarketplaceItemTesterScreenNow = ( url.indexOf(MARKETPLACE_ITEM_TESTER_QML_PATH) !== -1 || url === MARKETPLACE_ITEM_TESTER_QML_PATH); - + var onMarketplaceScreenNow = ( + url.indexOf(MARKETPLACE_QML_PATH) !== -1 || + url === MARKETPLACE_QML_PATH); if ((!onWalletScreenNow && onWalletScreen) || (!onCommerceScreenNow && onCommerceScreen) || (!onMarketplaceItemTesterScreenNow && onMarketplaceScreen) @@ -711,7 +731,7 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { onCommerceScreen = onCommerceScreenNow; onWalletScreen = onWalletScreenNow; onMarketplaceItemTesterScreen = onMarketplaceItemTesterScreenNow; - wireQmlEventBridge(onCommerceScreen || onWalletScreen || onMarketplaceItemTesterScreen); + wireQmlEventBridge(onCommerceScreen || onWalletScreen || onMarketplaceItemTesterScreen || onMarketplaceScreenNow); if (url === MARKETPLACE_PURCHASES_QML_PATH) { // FIXME? Is there a race condition here in which the event @@ -734,11 +754,8 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { Keyboard.raised = false; } - if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { - ContextOverlay.isInMarketplaceInspectionMode = true; - } else { - ContextOverlay.isInMarketplaceInspectionMode = false; - } + ContextOverlay.isInMarketplaceInspectionMode = false; + if (onInspectionCertificateScreen) { setCertificateInfo(contextOverlayEntity); @@ -769,16 +786,13 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { var BUTTON_NAME = "MARKET"; -var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; -// Append "?" if necessary to signal injected script that it's the initial page. -var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + (MARKETPLACE_URL.indexOf("?") > -1 ? "" : "?"); + var ui; function startup() { ui = new AppUi({ buttonName: BUTTON_NAME, sortOrder: 9, - inject: MARKETPLACES_INJECT_SCRIPT_URL, - home: MARKETPLACE_URL_INITIAL, + home: MARKETPLACE_QML_PATH, onScreenChanged: onTabletScreenChanged, onMessage: onQmlMessageReceived });