From ad471387f7212559fcbf34d4f37d1cbe0010ed85 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 14 Dec 2018 16:10:42 -0800 Subject: [PATCH] Integrate marketplace upload API --- .../resources/qml/controlsUit/Button.qml | 7 + .../resources/qml/hifi/AvatarPackager.qml | 3 +- .../qml/hifi/avatarPackager/AvatarProject.qml | 110 ++++++++- interface/src/Application.cpp | 2 +- interface/src/avatar/AvatarManager.cpp | 4 +- interface/src/avatar/AvatarPackager.cpp | 6 +- interface/src/avatar/AvatarPackager.h | 1 - interface/src/avatar/AvatarProject.cpp | 12 +- interface/src/avatar/AvatarProject.h | 7 +- .../src/avatar/MarketplaceItemUploader.cpp | 225 ++++++++++++++++-- .../src/avatar/MarketplaceItemUploader.h | 52 +++- scripts/system/html/js/entityProperties.js | 9 + 12 files changed, 386 insertions(+), 52 deletions(-) diff --git a/interface/resources/qml/controlsUit/Button.qml b/interface/resources/qml/controlsUit/Button.qml index 6ea7ce4b4c..9d92ff5e9a 100644 --- a/interface/resources/qml/controlsUit/Button.qml +++ b/interface/resources/qml/controlsUit/Button.qml @@ -28,6 +28,10 @@ Original.Button { width: hifi.dimensions.buttonWidth height: hifi.dimensions.controlLineHeight + property size implicitPadding: Qt.size(20, 16) + property int implicitWidth: content.implicitWidth + implicitPadding.width + property int implicitHeight: content.implicitHeight + implicitPadding.height + HifiConstants { id: hifi } onHoveredChanged: { @@ -89,6 +93,9 @@ Original.Button { } contentItem: Item { + id: content + implicitWidth: (buttonGlyph.visible ? buttonGlyph.implicitWidth : 0) + buttonText.implicitWidth + implicitHeight: buttonText.implicitHeight HiFiGlyphs { id: buttonGlyph; visible: control.buttonGlyph !== ""; diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 5a51a3c873..d5e21d9653 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -47,7 +47,7 @@ Windows.ScrollingWindow { anchors.top: parent.top anchors.topMargin: 25 anchors.bottomMargin: 25 - text: 'Avatar Packager' + text: 'Avatar Packager ' + parent.width + " " + parent.height } HifiControls.Button { @@ -72,6 +72,7 @@ Windows.ScrollingWindow { height: 30 onClicked: function() { var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; + var avatarProjectsPath = "C:/Users/ryanh/Documents/High Fidelity Avatars"; console.log("path = " + avatarProjectsPath); // TODO: make the dialog modal diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 085f1acdce..4f42927676 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -3,6 +3,8 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.0 +import QtQuick.Controls 2.2 as Original + import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 @@ -12,6 +14,7 @@ Item { HifiConstants { id: hifi } property int colorScheme; + property var uploader: undefined; visible: true @@ -58,13 +61,118 @@ Item { } HifiControls.Button { id: uploadButton + width: parent.width + height: 30 anchors.bottom: parent.bottom + text: qsTr("Upload") color: hifi.buttons.blue colorScheme: root.colorScheme - height: 30 onClicked: function() { + console.log("Uploading"); + parent.uploader = AvatarPackagerCore.currentAvatarProject.upload(); + console.log("uploader: "+ parent.uploader); + parent.uploader.uploadProgress.connect(function(uploaded, total) { + console.log("Uploader progress: " + uploaded + " / " + total); + }); + parent.uploader.completed.connect(function() { + try { + var response = JSON.parse(parent.uploader.responseData); + console.log("Uploader complete! " + response); + uploadStatus.text = response.status; + } catch (e) { + console.log("Error parsing JSON: " + parent.uploader.reponseData); + } + }); + parent.uploader.send(); } } + + Rectangle { + id: uploadingScreen + + visible: !!root.uploader + anchors.fill: parent + + 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 + } + } + + Item { + visible: !!root.uploader && root.uploader.complete + + anchors.fill: parent + + HiFiGlyphs { + id: successIcon + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + 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 + anchors.right: parent.right + + text: "View in Inventory" + + color: hifi.buttons.blue + colorScheme: root.colorScheme + onClicked: function() { + console.log("Opening in inventory"); + } + } + } + + Column { + Text { + id: uploadStatus + + text: "Uploading" + color: "white" + + } + Text { + text: "State: " + (!!root.uploader ? root.uploader.state : " NONE") + color: "white" + } + } + + } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7c4975e7a9..10f8d66855 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -460,7 +460,7 @@ public: // Don't actually crash in debug builds, in case this apparent deadlock is simply from // the developer actively debugging code #ifdef NDEBUG - deadlockDetectionCrash(); + //deadlockDetectionCrash(); #endif } } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 7ca18ca258..1a6b510ea1 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -536,6 +536,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // my avatar. (Other user machines will make a similar analysis and inject sound for their collisions.) if (collision.idA.isNull() || collision.idB.isNull()) { auto myAvatar = getMyAvatar(); + myAvatar->collisionWithEntity(collision); auto collisionSound = myAvatar->getCollisionSound(); if (collisionSound) { const auto characterController = myAvatar->getCharacterController(); @@ -571,9 +572,8 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents auto injector = AudioInjector::playSoundAndDelete(collisionSound, options); _collisionInjectors.emplace_back(injector); } - myAvatar->collisionWithEntity(collision); - return; } + return; } } } diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 3fdf193087..04ecc87067 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -25,6 +25,7 @@ std::once_flag setupQMLTypesFlag; AvatarPackager::AvatarPackager() { std::call_once(setupQMLTypesFlag, []() { qmlRegisterType(); + qmlRegisterType(); }); } @@ -47,8 +48,3 @@ QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { emit avatarProjectChanged(); return _currentAvatarProject; } - -QObject* AvatarPackager::uploadItem() { - std::vector filePaths; - return new MarketplaceItemUploader(QUuid(), filePaths); -} diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index f002631f17..def82b6311 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -35,7 +35,6 @@ signals: private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; //Q_INVOKABLE QObject* openAvatarProject(); - Q_INVOKABLE QObject* uploadItem(); AvatarProject* _currentAvatarProject{ nullptr }; }; diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index c7ea7e52ac..cc23027562 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -41,6 +41,9 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : _fstPath(fstPath), _fst(fstPath, FSTReader::readMapping(data)) { + _fstFilename = QFileInfo(_fstPath).fileName(); + qDebug() << "Pointers: " << this << &_fst; + _directory = QFileInfo(_fstPath).absoluteDir(); //_projectFiles = _directory.entryList(); @@ -51,13 +54,12 @@ AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : } void AvatarProject::appendDirectory(QString prefix, QDir dir) { - qDebug() << "Inside of " << prefix << dir.absolutePath(); auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; for (auto& entry : dir.entryInfoList({}, flags)) { if (entry.isFile()) { - _projectFiles.append(prefix + "/" + entry.fileName()); + //_projectFiles.append(prefix + "/" + entry.fileName()); + _projectFiles.append(entry.absoluteFilePath()); } else if (entry.isDir()) { - qDebug() << "Found dir " << entry.absoluteFilePath() << " in " << dir.absolutePath(); appendDirectory(prefix + dir.dirName() + "/", entry.absoluteFilePath()); } } @@ -67,3 +69,7 @@ void AvatarProject::refreshProjectFiles() { _projectFiles.clear(); appendDirectory("", _directory); } + +Q_INVOKABLE MarketplaceItemUploader* AvatarProject::upload() { + return new MarketplaceItemUploader("test_avatar", "blank description", _fstFilename, QUuid(), _projectFiles); +} diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 6dc64cda6f..1a0ed5cc5e 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -13,6 +13,7 @@ #ifndef hifi_AvatarProject_h #define hifi_AvatarProject_h +#include "MarketplaceItemUploader.h" #include "FST.h" #include @@ -38,10 +39,7 @@ public: return false; } - Q_INVOKABLE QObject* upload() { - // TODO: create new AvatarProjectUploader here, launch it and return it for status tracking in QML - return nullptr; - } + Q_INVOKABLE MarketplaceItemUploader* upload(); /** * returns the AvatarProject or a nullptr on failure. @@ -70,6 +68,7 @@ private: QStringList _projectFiles{}; QString _projectPath; QString _fstPath; + QString _fstFilename; }; #endif // hifi_AvatarProject_h diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index c2671aadec..7a5abacce4 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -14,44 +14,221 @@ #include #include -#include - -MarketplaceItemUploader::MarketplaceItemUploader(QUuid marketplaceID, std::vector filePaths) - : _filePaths(filePaths), _marketplaceID(marketplaceID) { +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +MarketplaceItemUploader::MarketplaceItemUploader(QString title, + QString description, + QString rootFilename, + QUuid marketplaceID, + QStringList filePaths) : + _title(title), + _description(description), _rootFilename(rootFilename), _filePaths(filePaths), _marketplaceID(marketplaceID) { + qWarning() << "File paths: " << _filePaths.join(", "); + //_marketplaceID = QUuid::fromString(QLatin1String("{50dbd62f-cb6b-4be4-afb8-1ef8bd2dffa8}")); +} + +void MarketplaceItemUploader::setState(State newState) { + qDebug() << "Setting uploader state to: " << newState; + + _state = newState; + emit stateChanged(newState); + if (newState == State::Complete) { + emit completed(); + } } void MarketplaceItemUploader::send() { + doGetCategories(); +} + +void MarketplaceItemUploader::doGetCategories() { + setState(State::GettingCategories); + + static const QString path = "/api/v1/marketplace/categories"; + auto accountManager = DependencyManager::get(); - auto request = accountManager->createRequest("/marketplace/item", AccountManagerAuth::Required); + auto request = accountManager->createRequest(path, AccountManagerAuth::None); + + qWarning() << "Request url is: " << request.url(); QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QByteArray data; - /* - auto reply = networkAccessManager.post(request, data); - - connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + 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) { - } else { - } - emit complete(); - }); - */ + auto extractCategoryID = [&doc]() -> std::pair { + auto items = doc.object()["data"].toObject()["items"]; + if (!items.isArray()) { + qWarning() << "Categories parse error: data.items is not an array"; + return { false, 0 }; + } - QTimer* timer = new QTimer(); - timer->setInterval(1000); - connect(timer, &QTimer::timeout, this, [this, timer]() { - if (progress <= 1.0f) { - progress += 0.1; - emit uploadProgress(progress * 100.0f, 100.0f); + auto itemsArray = items.toArray(); + for (const auto item : itemsArray) { + if (!item.isObject()) { + qWarning() << "Categories parse error: item is not an object"; + return { false, 0 }; + } + + auto itemObject = item.toObject(); + if (itemObject["name"].toString() == "Avatars") { + auto idValue = itemObject["id"]; + if (!idValue.isDouble()) { + qWarning() << "Categories parse error: id is not a number"; + return { false, 0 }; + } + return { true, (int)idValue.toDouble() }; + } + } + + qWarning() << "Categories parse error: could not find a category for 'Avatar'"; + return { false, 0 }; + }; + + bool success; + int id; + std::tie(success, id) = extractCategoryID(); + qDebug() << "Done " << success << id; + if (!success) { + qWarning() << "Failed to find marketplace category id"; + _error = Error::Unknown; + setState(State::Complete); + } else { + doUploadAvatar(); + } } else { - emit complete(); - timer->stop(); + _error = Error::Unknown; + setState(State::Complete); } }); - timer->start(); +} + +void MarketplaceItemUploader::doUploadAvatar() { + 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; + } + 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()); + } else { + reply = networkAccessManager.put(request, doc.toJson()); + } + + connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + _responseData = reply->readAll(); + qWarning() << "Finished request " << _responseData; + auto error = reply->error(); + if (error == QNetworkReply::NoError) { + doWaitForInventory(); + } else { + _error = Error::Unknown; + setState(State::Complete); + } + }); + + setState(State::UploadingAvatar); +} + +void MarketplaceItemUploader::doWaitForInventory() { + static const QString path = "/api/v1/commerce/inventory"; + + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest(path, AccountManagerAuth::Required); + + qWarning() << "Request url is: " << request.url(); + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkReply* reply = networkAccessManager.post(request, ""); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + auto data = reply->readAll(); + qWarning() << "Finished inventory request " << data; + + auto error = reply->error(); + if (error == QNetworkReply::NoError) { + } else { + _error = Error::Unknown; + } + setState(State::Complete); + }); } diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 2a3071244e..a0ec3f6991 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -20,36 +20,68 @@ class QNetworkReply; class MarketplaceItemUploader : public QObject { Q_OBJECT + + Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged) + Q_PROPERTY(State state READ getState NOTIFY stateChanged) + Q_PROPERTY(Error error READ getError) + Q_PROPERTY(QString responseData READ getResponseData) public: enum class Error { None, - ItemNotUpdateable, - ItemDoesNotExist, - RequestTimedOut, Unknown }; + Q_ENUM(Error); + enum class State { - Ready, - Sent + Idle, + GettingCategories, + UploadingAvatar, + WaitingForInventory, + Complete }; + Q_ENUM(State); - MarketplaceItemUploader(QUuid markertplaceID, std::vector filePaths); - - float progress{ 0.0f }; + MarketplaceItemUploader(QString title, + QString description, + QString rootFilename, + QUuid marketplaceID, + QStringList filePaths); Q_INVOKABLE void send(); + QString getResponseData() const { return _responseData; } + void setState(State newState); + State getState() const { return _state; } + bool getComplete() const { return _state == State::Complete; } + + Error getError() const { return _error; } + signals: void uploadProgress(qint64 bytesSent, qint64 bytesTotal); - void complete(); + void completed(); + void stateChanged(State newState); private: + void doGetCategories(); + void doUploadAvatar(); + void doWaitForInventory(); QNetworkReply* _reply; + + State _state{ State::Idle }; + Error _error{ Error::None }; + + QString _title; + QString _description; + QString _rootFilename; QUuid _marketplaceID; - std::vector _filePaths; + + QString _responseData; + + QStringList _filePaths; + QByteArray _fileData; }; #endif // hifi_MarketplaceItemUploader_h diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index dc304c6803..23c7346df8 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -112,6 +112,15 @@ const GROUPS = [ type: "color", propertyID: "color", }, + { + label: "Alpha", + type: "", + type: "number", + min: 0, + max: 1, + step: 0.001, + propertyID: "alpha", + }, ] }, {