Integrate marketplace upload API

This commit is contained in:
Ryan Huffman 2018-12-14 16:10:42 -08:00
parent 2269447741
commit ad471387f7
12 changed files with 386 additions and 52 deletions

View file

@ -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 !== "";

View file

@ -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

View file

@ -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"
}
}
}
}

View file

@ -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
}
}

View file

@ -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;
}
}
}

View file

@ -25,6 +25,7 @@ std::once_flag setupQMLTypesFlag;
AvatarPackager::AvatarPackager() {
std::call_once(setupQMLTypesFlag, []() {
qmlRegisterType<FST>();
qmlRegisterType<MarketplaceItemUploader>();
});
}
@ -47,8 +48,3 @@ QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) {
emit avatarProjectChanged();
return _currentAvatarProject;
}
QObject* AvatarPackager::uploadItem() {
std::vector<QString> filePaths;
return new MarketplaceItemUploader(QUuid(), filePaths);
}

View file

@ -35,7 +35,6 @@ signals:
private:
Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; };
//Q_INVOKABLE QObject* openAvatarProject();
Q_INVOKABLE QObject* uploadItem();
AvatarProject* _currentAvatarProject{ nullptr };
};

View file

@ -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);
}

View file

@ -13,6 +13,7 @@
#ifndef hifi_AvatarProject_h
#define hifi_AvatarProject_h
#include "MarketplaceItemUploader.h"
#include "FST.h"
#include <QDir>
@ -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

View file

@ -14,44 +14,221 @@
#include <AccountManager.h>
#include <DependencyManager.h>
#include <qtimer.h>
MarketplaceItemUploader::MarketplaceItemUploader(QUuid marketplaceID, std::vector<QString> filePaths)
: _filePaths(filePaths), _marketplaceID(marketplaceID) {
#include <QBuffer>
#include <quazip5\quazip.h>
#include <quazip5\quazipfile.h>
#include <qtimer.h>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
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<AccountManager>();
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<bool, int> {
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<AccountManager>();
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<AccountManager>();
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);
});
}

View file

@ -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<QString> 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<QString> _filePaths;
QString _responseData;
QStringList _filePaths;
QByteArray _fileData;
};
#endif // hifi_MarketplaceItemUploader_h

View file

@ -112,6 +112,15 @@ const GROUPS = [
type: "color",
propertyID: "color",
},
{
label: "Alpha",
type: "",
type: "number",
min: 0,
max: 1,
step: 0.001,
propertyID: "alpha",
},
]
},
{