Update uploader screen

This commit is contained in:
Ryan Huffman 2018-12-20 16:30:08 -08:00
parent 202326dbac
commit 556f516be6
8 changed files with 339 additions and 203 deletions

View file

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

View file

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

View file

@ -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 wont 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: '<b>File:</b> ' + 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. <a href='toggle'>" + (fileList.visible ? "Hide" : "Show") + " list</a>"
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"
}
}
}
}

View file

@ -17,8 +17,9 @@
#include <QFileInfo>
#include <QDebug>
#include <QQmlEngine>
#include "FBXSerializer.h"
#include <QTimer>
#include "FBXSerializer.h"
#include <ui/TabletScriptingInterface.h>
#include "scripting/HMDScriptingInterface.h"
@ -57,20 +58,16 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectNa
std::shared_ptr<hfm::Model> 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<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system"));
tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml");
DependencyManager::get<HMDScriptingInterface>()->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<TabletProxy*>(
DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system"));
tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } }));
});
}

View file

@ -41,7 +41,7 @@ public:
return false;
}
Q_INVOKABLE MarketplaceItemUploader* upload();
Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting);
Q_INVOKABLE void openInInventory();
/**

View file

@ -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<bool, int> {
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<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());
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<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();
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<AccountManager>();
auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
auto accountManager = DependencyManager::get<AccountManager>();
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(); });
}
}
});
}

View file

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

View file

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