From 556f516be669721be33e9537e930a93017ecb792 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 20 Dec 2018 16:30:08 -0800 Subject: [PATCH] Update uploader screen --- .../resources/qml/hifi/AvatarPackager.qml | 25 +- .../avatarPackager/AvatarPackagerFooter.qml | 6 +- .../qml/hifi/avatarPackager/AvatarProject.qml | 223 +++++++++------- interface/src/avatar/AvatarProject.cpp | 39 ++- interface/src/avatar/AvatarProject.h | 2 +- .../src/avatar/MarketplaceItemUploader.cpp | 240 +++++++++++------- .../src/avatar/MarketplaceItemUploader.h | 5 + libraries/fbx/src/FST.h | 2 + 8 files changed, 339 insertions(+), 203 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 8eb765716e..4d27e6fce4 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -8,6 +8,7 @@ import "../stylesUit" 1.0 import "../windows" as Windows import "../dialogs" import "avatarPackager" +import "avatarapp" 1.0 as AvatarApp Windows.ScrollingWindow { id: root @@ -27,18 +28,14 @@ Windows.ScrollingWindow { id: windowContent height: pane.height width: pane.width - anchors.fill: parent - // FIXME: modal overlay does not show - Rectangle { - id: modalOverlay + AvatarApp.MessageBox { + id: popup anchors.fill: parent - z: 20000 - color: "#aa031b33" - clip: true - visible: true + visible: false } + // FIXME: modal overlay does not show Column { id: avatarPackager anchors.fill: parent @@ -61,6 +58,12 @@ Windows.ScrollingWindow { PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } PropertyChanges { target: avatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } + }, + State { + name: "project-upload" + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarUploader; visible: true } + PropertyChanges { target: avatarPackagerFooter; color: "blue"; visible: false } } ] @@ -86,6 +89,12 @@ Windows.ScrollingWindow { anchors.fill: parent } + AvatarProjectUpload { + id: avatarUploader + anchors.fill: parent + root: avatarProject + } + CreateAvatarProject { id: createAvatarProject colorScheme: root.colorScheme diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml index 526a2047e3..8498d20858 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -7,10 +7,10 @@ Rectangle { id: avatarPackagerFooter color: "#404040" - height: 74 + height: content === defaultContent ? 0 : 74 width: parent.width - property var content: Item { } + property var content: Item { id: defaultContent } children: [background, content] @@ -21,4 +21,4 @@ Rectangle { border.width: 2; } -} +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 0aca722352..286ebb23ba 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -25,6 +25,7 @@ Item { anchors.rightMargin: 17 HifiControls.Button { id: uploadButton + enabled: Account.loggedIn //width: parent.width //anchors.bottom: parent.bottom anchors.verticalCenter: parent.verticalCenter @@ -35,26 +36,82 @@ Item { width: 133 height: 40 onClicked: function() { - console.log("Uploading"); - root.uploader = AvatarPackagerCore.currentAvatarProject.upload(); - console.log("uploader: "+ root.uploader); - root.uploader.uploadProgress.connect(function(uploaded, total) { - console.log("Uploader progress: " + uploaded + " / " + total); - }); - root.uploader.completed.connect(function() { - try { - var response = JSON.parse(root.uploader.responseData); - console.log("Uploader complete! " + response); - uploadStatus.text = response.status; - } catch (e) { - console.log("Error parsing JSON: " + root.uploader.reponseData); - } - }); - root.uploader.send(); + if (AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID()) { + showConfirmUploadPopup(uploadNew, uploadUpdate); + } else { + uploadNew(); + } } } } + function uploadNew() { + console.log("Uploading new"); + upload(false); + } + function uploadUpdate() { + console.log("Uploading update"); + upload(true); + } + + function upload(updateExisting) { + root.uploader = AvatarPackagerCore.currentAvatarProject.upload(updateExisting); + console.log("uploader: "+ root.uploader); + root.uploader.uploadProgress.connect(function(uploaded, total) { + console.log("Uploader progress: " + uploaded + " / " + total); + }); + root.uploader.completed.connect(function() { + try { + var response = JSON.parse(root.uploader.responseData); + console.log("Uploader complete! " + response); + uploadStatus.text = response.status; + } catch (e) { + console.log("Error parsing JSON: " + root.uploader.reponseData); + } + }); + root.uploader.send(); + avatarPackager.state = "project-upload"; + } + + function showConfirmUploadPopup() { + popup.titleText = 'Overwrite Avatar' + popup.bodyText = 'You have previously uploaded the avatar file from this project.' + + ' This will overwrite that avatar and you won’t be able to access the older version.' + + popup.button1text = 'CREATE NEW'; + popup.button2text = 'OVERWRITE'; + + popup.onButton2Clicked = function() { + popup.close(); + uploadUpdate(); + } + popup.onButton1Clicked = function() { + popup.close(); + showConfirmCreateNewPopup(); + }; + + popup.open(); + //popup.forceActiveFocus(); + } + + function showConfirmCreateNewPopup(confirmCallback) { + popup.titleText = 'Create New' + popup.bodyText = 'This will upload your current files with the same avatar name.' + + ' You will lose the ability to update the previously uploaded avatar. Are you sure you want to continue?' + + popup.button1text = 'CANCEL'; + popup.button2text = 'CONFIRM'; + + popup.onButton1Clicked = function() { popup.close() }; + popup.onButton2Clicked = function() { + popup.close(); + uploadNew(); + }; + + popup.open(); + //popup.forceActiveFocus(); + } + RalewaySemiBold { id: avatarFBXNameLabel size: 14 @@ -79,11 +136,15 @@ Item { } Rectangle { + id: fileList + + visible: false + color: "white" - visible: AvatarPackagerCore.currentAvatarProject !== null anchors.top: openFolderButton.bottom anchors.left: parent.left anchors.right: parent.right + anchors.bottom: showFilesText.top //anchors.bottom: uploadButton.top anchors.topMargin: 10 anchors.bottomMargin: 10 @@ -92,96 +153,82 @@ Item { ListView { anchors.fill: parent model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles - delegate: Text { text: 'File: ' + modelData } + delegate: Rectangle { + width: parent.width + height: fileText.implicitHeight + 10 + color: (index % 2 == 0) ? "white" : "grey" + Text { + id: fileText + anchors.top: parent.top + 5 + anchors.left: parent.left + 5 + text: modelData + } + } } } + Text { + id: showFilesText + + visible: AvatarPackagerCore.currentAvatarProject !== null + + anchors.bottom: loginRequiredMessage.top + anchors.bottomMargin: 10 + + font.pointSize: 12 + text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in the project. " + (fileList.visible ? "Hide" : "Show") + " list" + + onLinkActivated: fileList.visible = !fileList.visible + } + Rectangle { - id: uploadingScreen + id: loginRequiredMessage - visible: !!root.uploader - anchors.fill: parent + visible: !Account.loggedIn + height: loginRequiredTextRow.height + 20 - color: "black" - - Item { - visible: !!root.uploader && !root.uploader.complete - - anchors.fill: parent - - AnimatedImage { - id: uploadSpinner - - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - - source: "../../../icons/loader-snake-64-w.gif" - playing: true - z: 10000 - } + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right } - Item { - visible: !!root.uploader && root.uploader.complete + color: "#FFD6AD" + border.color: "#F39622" + border.width: 2 + radius: 2 + + Item { + id: loginRequiredTextRow + + height: Math.max(loginWarningGlyph.implicitHeight, loginWarningText.implicitHeight) anchors.fill: parent + anchors.margins: 10 HiFiGlyphs { - id: successIcon + id: loginWarningGlyph - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left - size: 128 - text: "\ue01a" - color: "#1FC6A6" - } - - Text { - text: "Congratulations!" - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: successIcon.bottom - - color: "white" - } - - HifiControls.Button { width: implicitWidth - height: implicitHeight - anchors.bottom: parent.bottom + font.pointSize: 20 + text: "+" + color: "black" + } + Text { + id: loginWarningText + + anchors.verticalCenter: parent.verticalCenter + anchors.left: loginWarningGlyph.right anchors.right: parent.right - text: "View in Inventory" - - color: hifi.buttons.blue - colorScheme: root.colorScheme - onClicked: function() { - console.log("Opening in inventory"); - - AvatarPackagerCore.currentAvatarProject.openInInventory(); - } + text: "Please login to upload your avatar to High Fidelity hosting." + font.pointSize: 12 + wrapMode: Text.Wrap } } - - Column { - Text { - id: uploadStatus - - text: "Uploading" - color: "white" - - } - Text { - text: "State: " + (!!root.uploader ? root.uploader.state : " NONE") - color: "white" - } - } - } } diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 7ba9d395eb..4a14479cce 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -17,8 +17,9 @@ #include #include #include -#include "FBXSerializer.h" +#include +#include "FBXSerializer.h" #include #include "scripting/HMDScriptingInterface.h" @@ -57,20 +58,16 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectNa std::shared_ptr hfmModel; - try { qDebug() << "Reading FBX file : " << fbxInfo.filePath(); const QByteArray fbxContents = fbx.readAll(); hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath()); - } - catch (const QString& error) { + } catch (const QString& error) { qDebug() << "Error reading: " << error; return nullptr; } //TODO: copy/fix textures here: - - FST* fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel); fst->setName(avatarProjectName); @@ -89,7 +86,6 @@ bool AvatarProject::isValidNewProjectName(const QString& projectName) { AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) { - } AvatarProject::AvatarProject(FST* fst) { _fst = fst; @@ -119,8 +115,22 @@ void AvatarProject::refreshProjectFiles() { appendDirectory("", _directory); } -MarketplaceItemUploader* AvatarProject::upload() { - return new MarketplaceItemUploader("test_avatar", "blank description", QFileInfo(getFSTPath()).fileName(), QUuid(), _projectFiles); +MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { + QUuid itemID; + if (updateExisting) { + itemID = _fst->getMarketplaceID(); + } + auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), itemID, + _projectFiles); + connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { + if (uploader->getError() == MarketplaceItemUploader::Error::None) { + _fst->setMarketplaceID(uploader->getMarketplaceID()); + // TODO(thoys) uncomment this + //_fst->write(); + } + }); + + return uploader; } void AvatarProject::openInInventory() { @@ -128,7 +138,12 @@ void AvatarProject::openInInventory() { DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); DependencyManager::get()->openTablet(); - tablet->sendToQml(QVariantMap({ - { "method", "updatePurchases" }, - { "filterText", "filtertext" } })); + auto name = getProjectName(); + + // I'm not a fan of this, but it's the only current option. + QTimer::singleShot(1000, [name]() { + auto tablet = dynamic_cast( + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } })); + }); } diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 023cd7b079..b164e67e0d 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -41,7 +41,7 @@ public: return false; } - Q_INVOKABLE MarketplaceItemUploader* upload(); + Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory(); /** diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 7a5abacce4..1559f359a7 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -67,10 +67,9 @@ void MarketplaceItemUploader::doGetCategories() { QNetworkReply* reply = networkAccessManager.get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { - auto doc = QJsonDocument::fromJson(reply->readAll()); - auto error = reply->error(); if (error == QNetworkReply::NoError) { + auto doc = QJsonDocument::fromJson(reply->readAll()); auto extractCategoryID = [&doc]() -> std::pair { auto items = doc.object()["data"].toObject()["items"]; if (!items.isArray()) { @@ -119,116 +118,175 @@ void MarketplaceItemUploader::doGetCategories() { } void MarketplaceItemUploader::doUploadAvatar() { - QBuffer buffer{ &_fileData }; - //buffer.open(QIODevice::WriteOnly); - QuaZip zip{ &buffer }; - if (!zip.open(QuaZip::Mode::mdAdd)) { - qWarning() << "Failed to open zip!!"; + QBuffer buffer{ &_fileData }; + //buffer.open(QIODevice::WriteOnly); + QuaZip zip{ &buffer }; + if (!zip.open(QuaZip::Mode::mdAdd)) { + qWarning() << "Failed to open zip!!"; + } + + for (auto& filePath : _filePaths) { + qWarning() << "Zipping: " << filePath; + QFileInfo fileInfo{ filePath }; + + QuaZipFile zipFile{ &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { + qWarning() << "Could not open zip file:" << zipFile.getZipError(); + _error = Error::Unknown; + setState(State::Complete); + return; } - - for (auto& filePath : _filePaths) { - qWarning() << "Zipping: " << filePath; - QFileInfo fileInfo{ filePath }; - - QuaZipFile zipFile{ &zip }; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { - qWarning() << "Could not open zip file:" << zipFile.getZipError(); - _error = Error::Unknown; - setState(State::Complete); - return; - } - QFile file{ filePath }; - if (file.open(QIODevice::ReadOnly)) { - zipFile.write(file.readAll()); - } else { - qWarning() << "Failed to open: " << filePath; - } - file.close(); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qWarning() << "Could not close zip file: " << zipFile.getZipError(); - setState(State::Complete); - return; - } - } - - zip.close(); - - qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB"; - - QString path = "/api/v1/marketplace/items"; - bool creating = true; - if (!_marketplaceID.isNull()) { - creating = false; - auto idWithBraces = _marketplaceID.toString(); - auto idWithoutBraces = idWithBraces.mid(1, idWithBraces.length() - 2); - path += "/" + idWithoutBraces; - } - auto accountManager = DependencyManager::get(); - auto request = accountManager->createRequest(path, AccountManagerAuth::Required); - qWarning() << "Request url is: " << request.url(); - - QJsonObject root{ { "marketplace_item", - QJsonObject{ { "title", _title }, - { "description", _description }, - { "root_file_key", _rootFilename }, - { "category_ids", QJsonArray({ 5 }) }, - //{ "attributions", QJsonArray({ QJsonObject{ { "name", "" }, { "link", "" } } }) }, - { "license", 0 }, - { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - QJsonDocument doc{ root }; - - qWarning() << "data: " << doc.toJson(); - - _fileData.toBase64(); - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - - QNetworkReply* reply{ nullptr }; - if (creating) { - reply = networkAccessManager.post(request, doc.toJson()); + QFile file{ filePath }; + if (file.open(QIODevice::ReadOnly)) { + zipFile.write(file.readAll()); } else { - reply = networkAccessManager.put(request, doc.toJson()); + qWarning() << "Failed to open: " << filePath; } + file.close(); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qWarning() << "Could not close zip file: " << zipFile.getZipError(); + setState(State::Complete); + return; + } + } - connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + zip.close(); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { + qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB"; + + QString path = "/api/v1/marketplace/items"; + bool creating = true; + if (!_marketplaceID.isNull()) { + creating = false; + auto idWithBraces = _marketplaceID.toString(); + auto idWithoutBraces = idWithBraces.mid(1, idWithBraces.length() - 2); + path += "/" + idWithoutBraces; + } + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest(path, AccountManagerAuth::Required); + qWarning() << "Request url is: " << request.url(); + + QJsonObject root{ { "marketplace_item", + QJsonObject{ { "title", _title }, + { "description", _description }, + { "root_file_key", _rootFilename }, + { "category_ids", QJsonArray({ 5 }) }, + //{ "attributions", QJsonArray({ QJsonObject{ { "name", "" }, { "link", "" } } }) }, + { "license", 0 }, + { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + QJsonDocument doc{ root }; + + qWarning() << "data: " << doc.toJson(); + + _fileData.toBase64(); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkReply* reply{ nullptr }; + if (creating) { + reply = networkAccessManager.post(request, doc.toJson()); + } else { + reply = networkAccessManager.put(request, doc.toJson()); + } + + connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + //_responseData = reply->readAll(); + auto error = reply->error(); + if (error == QNetworkReply::NoError) { _responseData = reply->readAll(); qWarning() << "Finished request " << _responseData; - auto error = reply->error(); - if (error == QNetworkReply::NoError) { + + auto doc = QJsonDocument::fromJson(_responseData.toLatin1()); + auto status = doc.object()["status"].toString(); + if (status == "success") { + _marketplaceID = QUuid::fromString(doc["data"].toObject()["marketplace_id"].toString()); + _itemVersion = doc["data"].toObject()["version"].toDouble(); doWaitForInventory(); } else { _error = Error::Unknown; setState(State::Complete); } - }); + } else { + _error = Error::Unknown; + setState(State::Complete); + } + }); - setState(State::UploadingAvatar); + setState(State::UploadingAvatar); } void MarketplaceItemUploader::doWaitForInventory() { - static const QString path = "/api/v1/commerce/inventory"; + static const QString path = "/api/v1/commerce/inventory"; - auto accountManager = DependencyManager::get(); - auto request = accountManager->createRequest(path, AccountManagerAuth::Required); + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest(path, AccountManagerAuth::Required); - qWarning() << "Request url is: " << request.url(); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkReply* reply = networkAccessManager.post(request, ""); - QNetworkReply* reply = networkAccessManager.post(request, ""); + _numRequestsForInventory++; - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - auto data = reply->readAll(); - qWarning() << "Finished inventory request " << data; + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + auto data = reply->readAll(); - auto error = reply->error(); - if (error == QNetworkReply::NoError) { - } else { - _error = Error::Unknown; - } + bool success = false; + + auto error = reply->error(); + if (error == QNetworkReply::NoError) { + // Parse response data + auto doc = QJsonDocument::fromJson(data); + auto isAssetAvailable = [this, &doc]() -> bool { + if (!doc.isObject()) { + return false; + } + auto root = doc.object(); + auto status = root["status"].toString(); + if (status != "success") { + return false; + } + auto data = root["data"]; + if (!data.isObject()) { + return false; + } + auto assets = data.toObject()["assets"]; + if (!assets.isArray()) { + return false; + } + for (auto asset : assets.toArray()) { + auto assetObject = asset.toObject(); + auto id = QUuid::fromString(assetObject["id"].toString()); + if (id.isNull()) { + continue; + } + if (id == _marketplaceID) { + auto version = assetObject["version"]; + if (version.isDouble()) { + int versionInt = version.toDouble(); + if (versionInt >= _itemVersion) { + return true; + } + } + } + } + return false; + }; + + success = isAssetAvailable(); + } + if (success) { setState(State::Complete); - }); + } else { + qDebug() << "Failed to find item in inventory"; + if (_numRequestsForInventory > 8) { + _error = Error::Unknown; + setState(State::Complete); + } else { + QTimer::singleShot(5000, [this]() { doWaitForInventory(); }); + } + } + }); } diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index a0ec3f6991..9cfd531aca 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -56,6 +56,8 @@ public: State getState() const { return _state; } bool getComplete() const { return _state == State::Complete; } + QUuid getMarketplaceID() const { return _marketplaceID; } + Error getError() const { return _error; } signals: @@ -77,9 +79,12 @@ private: QString _description; QString _rootFilename; QUuid _marketplaceID; + int _itemVersion; QString _responseData; + int _numRequestsForInventory{ 0 }; + QStringList _filePaths; QByteArray _fileData; }; diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 524463b721..83bb1e1933 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -37,7 +37,9 @@ public: QString getModelPath() const { return _modelPath; } void setModelPath(const QString& modelPath); + Q_INVOKABLE bool hasMarketplaceID() const { return !_marketplaceID.isNull(); } QUuid getMarketplaceID() const { return _marketplaceID; } + void setMarketplaceID(QUuid marketplaceID) { _marketplaceID = marketplaceID; } QString getPath() { return _fstPath; }