From 1da179dc04973c7c5b3d7d8055d97e55ad46230e Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 27 Dec 2018 00:13:45 -0800 Subject: [PATCH] Update avatar packager styling --- .../resources/images/loader-snake-128.png | Bin 0 -> 463 bytes .../resources/qml/hifi/AvatarPackager.qml | 1 + .../qml/hifi/avatarPackager/AvatarProject.qml | 159 +++++++++++++++--- .../avatarPackager/AvatarProjectUpload.qml | 94 +++++++---- .../avatarPackager/AvatarUploadStatusItem.qml | 96 +++++++++++ .../qml/hifi/avatarPackager/LoadingCircle.qml | 20 +++ .../qml/hifi/avatarapp/MessageBox.qml | 16 ++ interface/src/avatar/AvatarProject.h | 3 +- .../src/avatar/MarketplaceItemUploader.cpp | 23 ++- .../src/avatar/MarketplaceItemUploader.h | 6 +- libraries/fbx/src/FST.cpp | 5 + libraries/fbx/src/FST.h | 6 +- 12 files changed, 368 insertions(+), 61 deletions(-) create mode 100644 interface/resources/images/loader-snake-128.png create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml diff --git a/interface/resources/images/loader-snake-128.png b/interface/resources/images/loader-snake-128.png new file mode 100644 index 0000000000000000000000000000000000000000..b8ee57766488aed3627111d0dd6a1f0bb2a7dbaf GIT binary patch literal 463 zcmV;=0WkiFP)w!o@;$>0gGA+Fq)u?0a-$u1CxnfTGIa^5mSWSEh$GnaXjy&^`e zPrPbH%)n^08iN;ay=%^xyPw!L}2gYuE!%c-{nAkChx8*Uz>Z2ojhOq=5uHLPX`@xu3{>6{a~(gmcGG% zq9U~swXRF%aQMzSpPDWV0wFHD7~H-YW8$pIiA3Vu_ynabg<_e%Q|SNz002ovPDHLk FV1jQ+#bE#d literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 0dea524991..46fd98daab 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -48,6 +48,7 @@ Windows.ScrollingWindow { id: popup anchors.fill: parent visible: false + closeOnClickOutside: true } Column { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 81afacf28c..d4a67f31fb 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -19,35 +19,130 @@ Item { property int colorScheme; property var uploader: undefined; + property bool hasSuccessfullyUploaded: true; + visible: false anchors.fill: parent anchors.margins: 10 property var footer: Item { - id: uploadFooter - anchors.fill: parent - anchors.rightMargin: 17 - HifiControls.Button { - id: uploadButton - enabled: Account.loggedIn - //width: parent.width - //anchors.bottom: parent.bottom - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - text: qsTr("Upload") - color: hifi.buttons.blue - colorScheme: root.colorScheme - width: 133 - height: 40 - onClicked: function() { - if (AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID()) { - showConfirmUploadPopup(uploadNew, uploadUpdate); - } else { + Item { + id: uploadFooter + + visible: !root.uploader || root.finished || root.uploader.state !== 4 + + anchors.fill: parent + anchors.rightMargin: 17 + + HifiControls.Button { + id: uploadButton + + visible: !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded + enabled: Account.loggedIn + //width: parent.width + //anchors.bottom: parent.bottom + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + text: qsTr("Upload") + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: 133 + height: 40 + onClicked: function() { uploadNew(); } } + HifiControls.Button { + id: updateButton + + visible: AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded + enabled: Account.loggedIn + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + + text: qsTr("Update") + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: 134 + height: 40 + onClicked: function() { + showConfirmUploadPopup(uploadNew, uploadUpdate); + } + } + Item { + anchors.fill: parent + visible: root.hasSuccessfullyUploaded + + HifiControls.Button { + enabled: Account.loggedIn + + anchors.verticalCenter: parent.verticalCenter + anchors.right: viewInInventoryButton.left + anchors.rightMargin: 16 + + text: qsTr("Update") + color: hifi.buttons.white + colorScheme: root.colorScheme + width: 134 + height: 40 + onClicked: function() { + showConfirmUploadPopup(uploadNew, uploadUpdate); + } + } + HifiControls.Button { + id: viewInInventoryButton + + enabled: Account.loggedIn + + width: 168 + height: 40 + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + + text: qsTr("View in Inventory") + color: hifi.buttons.blue + colorScheme: root.colorScheme + + onClicked: AvatarPackagerCore.currentAvatarProject.openInInventory() + } + } + } + + Rectangle { + id: uploadingItemFooter + + anchors.fill: parent + anchors.topMargin: 1 + visible: !!root.uploader && !root.finished && root.uploader.state === 4 + + color: "#00B4EF" + + LoadingCircle { + id: runningImage + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 16 + + width: 28 + height: 28 + } + RalewayRegular { + id: stepText + + size: 20 + + anchors.verticalCenter: parent.verticalCenter + anchors.left: runningImage.right + anchors.leftMargin: 16 + + text: "Adding item to Inventory" + color: "white" + } } } @@ -66,7 +161,10 @@ Item { root.uploader.uploadProgress.connect(function(uploaded, total) { console.log("Uploader progress: " + uploaded + " / " + total); }); - root.uploader.finished.connect(function() { + root.uploader.completed.connect(function() { + root.hasSuccessfullyUploaded = true; + }); + root.uploader.finishedChanged.connect(function() { try { var response = JSON.parse(root.uploader.responseData); console.log("Uploader complete! " + response); @@ -121,6 +219,25 @@ Item { RalewayRegular { id: infoMessage + states: [ + State { + when: root.hasSuccessfullyUploaded + name: "upload-success" + PropertyChanges { + target: infoMessage + text: "Your avatar has been uploaded to our servers. You can modify the project files and update it again to make changes on the uploaded avatar." + } + }, + State { + name: "has-previous-success" + when: !!AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID + PropertyChanges { + target: infoMessage + text: "Click \"Update\" to overwrite the hosted files and update the avatar in your inventory. You will have to “Wear” the avatar again to see changes." + } + } + ] + color: 'white' size: 20 @@ -132,7 +249,7 @@ Item { wrapMode: Text.Wrap - text: "Click \"Update\" to overwrite the hosted files and update the avatar in your inventory. You will have to “Wear” the avatar again to see changes." + text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users." } HifiControls.Button { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index 6e011b1ec7..8b80df3d95 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -8,28 +8,35 @@ import QtQuick.Controls 2.2 as Original import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 - Item { id: uploadingScreen property var root: undefined - //visible: !!root.uploader visible: false anchors.fill: parent Timer { id: backToProjectTimer - interval: 2000 + interval: 5000 running: false repeat: false - onTriggered: avatarPackager.state = "project" + onTriggered: { + if (avatarPackager.state =="project-upload") { + avatarPackager.state = "project" + } + } } + function stateChangedCallback(newState) { + if (newState >= 4) { + root.uploader.stateChanged.disconnect(stateChangedCallback) + backToProjectTimer.start(); + } + } onVisibleChanged: { - console.log("Visibility changed"); if (visible) { - root.uploader.finished.connect(function() { - console.log("Did complete"); + root.uploader.stateChanged.connect(stateChangedCallback); + root.uploader.finishedChanged.connect(function() { backToProjectTimer.start(); }); } @@ -46,48 +53,62 @@ Item { id: statusItem width: parent.width - height: 128 + height: 192 - AnimatedImage { + states: [ + State { + name: "success" + when: !!root.uploader && root.uploader.state >= 4 && root.uploader.error === 0 + PropertyChanges { target: uploadSpinner; visible: false } + PropertyChanges { target: errorIcon; visible: false } + PropertyChanges { target: successIcon; visible: true } + }, + State { + name: "error" + when: !!root.uploader && root.uploader.finished && root.uploader.error !== 0 + PropertyChanges { target: uploadSpinner; visible: false } + PropertyChanges { target: errorIcon; visible: true } + PropertyChanges { target: successIcon; visible: false } + } + ] + + LoadingCircle { id: uploadSpinner - visible: !!root.uploader && !root.uploader.complete + visible: true anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } - - source: "../../../icons/loader-snake-64-w.gif" - playing: true - z: 10000 } HiFiGlyphs { id: errorIcon - visible: !!root.uploader && root.uploader.complete && root.uploader.error !== 0 + + visible: false anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } - size: 128 + size: 164 text: "w" - color: "red" + color: "#EA4C5F" } HiFiGlyphs { id: successIcon - visible: !!root.uploader && root.uploader.complete && root.uploader.error === 0 + visible: false anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } - size: 128 + size: 164 text: "\ue01a" color: "#1FC6A6" } @@ -96,34 +117,49 @@ Item { id: statusRows anchors.top: statusItem.bottom + anchors.left: parent.left + anchors.leftMargin: 12 AvatarUploadStatusItem { id: statusCategories + uploader: root.uploader text: "Retreiving categories" - state: root.uploader.state == 1 ? "running" : (root.uploader.state > 1 ? "success" : (root.uploader.error ? "fail" : "")) + uploaderState: 1 } AvatarUploadStatusItem { id: statusUploading + uploader: root.uploader anchors.top: statusCategories.bottom text: "Uploading data" - state: root.uploader.state == 2 ? "running" : (root.uploader.state > 2 ? "success" : (root.uploader.error ? "fail" : "")) + uploaderState: 2 } - // TODO add waiting for response - //AvatarUploadStatusItem { - //id: statusResponse - //text: "Waiting for completion" - //} AvatarUploadStatusItem { - id: statusInventory + id: statusResponse + uploader: root.uploader anchors.top: statusUploading.bottom - text: "Waiting for inventory" + text: "Waiting for response" - state: root.uploader.state == 3 ? "running" : (root.uploader.state > 3 ? "success" : (root.uploader.error ? "fail" : "")) + uploaderState: 3 } } + RalewayRegular { + visible: root.uploader.error + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + + size: 28 + wrapMode: Text.Wrap + color: "white" + text: "We couldn't upload your avatar at this time. Please try again later." + } } Column { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml new file mode 100644 index 0000000000..d93afbd4e8 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml @@ -0,0 +1,96 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Item { + id: root + + height: 48 + + property string text: "NO STEP TEXT" + property int uploaderState; + property var uploader; + + state: root.uploader.state > uploaderState + ? "success" + : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : "")) + + states: [ + State { + name: "running" + PropertyChanges { target: stepText; color: "white" } + PropertyChanges { target: runningImage; visible: true; playing: true } + }, + State { + name: "fail" + PropertyChanges { target: stepText; color: "#EA4C5F" } + PropertyChanges { target: failGlyph; visible: true } + }, + State { + name: "success" + PropertyChanges { target: stepText; color: "white" } + PropertyChanges { target: successGlyph; visible: true } + } + ] + + Item { + id: statusItem + + width: 48 + height: parent.height + + AnimatedImage { + id: runningImage + + visible: false + + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + width: 32 + height: 32 + + source: "../../../icons/loader-snake-64-w.gif" + playing: false + } + HiFiGlyphs { + id: successGlyph + + visible: false + + width: implicitWidth + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + size: 48 + text: "\ue01a" + color: "#1FC6A6" + } + HiFiGlyphs { + id: failGlyph + + visible: false + + width: implicitWidth + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + size: 48 + text: "+" + color: "#EA4C5F" + } + } + RalewayRegular { + id: stepText + + anchors.left: statusItem.right + anchors.verticalCenter: parent.verticalCenter + + text: root.text + size: 28 + color: "#777777" + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml new file mode 100644 index 0000000000..f6ba81a96f --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml @@ -0,0 +1,20 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 + +Image { + id: root + + width: 128 + height: 128 + + source: "../../../images/loader-snake-128.png" + + RotationAnimation on rotation { + duration: 2000 + loops: Animation.Infinite + from: 0 + to: 360 + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarapp/MessageBox.qml b/interface/resources/qml/hifi/avatarapp/MessageBox.qml index 1834364fe4..88f7f888cb 100644 --- a/interface/resources/qml/hifi/avatarapp/MessageBox.qml +++ b/interface/resources/qml/hifi/avatarapp/MessageBox.qml @@ -23,6 +23,8 @@ Rectangle { property string button2color: hifi.buttons.blue; property string button2text: '' + property bool closeOnClickOutside: false; + property var onButton2Clicked; property var onButton1Clicked; property var onLinkClicked; @@ -56,6 +58,11 @@ Rectangle { anchors.fill: parent; propagateComposedEvents: false; hoverEnabled: true; + onClicked: { + if (closeOnClickOutside) { + root.close() + } + } } Rectangle { @@ -68,6 +75,15 @@ Rectangle { console.debug('mainContainer: height = ', height) } + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + onClicked: function(ev) { + ev.accepted = true; + } + } + anchors.centerIn: parent color: "white" diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index d5cd7762a1..6da9f710cc 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -34,7 +34,7 @@ class AvatarProject : public QObject { Q_PROPERTY(QString projectFolderPath READ getProjectPath) Q_PROPERTY(QString projectFSTPath READ getFSTPath) Q_PROPERTY(QString projectFBXPath READ getFBXPath) - Q_PROPERTY(QString name READ getProjectName) + Q_PROPERTY(QString name READ getProjectName NOTIFY nameChanged) public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); @@ -57,6 +57,7 @@ public: } signals: + void nameChanged(); void projectFilesChanged(); private: diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 0a14ea9af4..31dcf8e9a0 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -39,6 +39,8 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, } void MarketplaceItemUploader::setState(State newState) { + Q_ASSERT(_state != State::Complete); + Q_ASSERT(_error == Error::None); Q_ASSERT(newState != _state); qDebug() << "Setting uploader state to: " << newState; @@ -46,16 +48,17 @@ void MarketplaceItemUploader::setState(State newState) { emit stateChanged(newState); if (newState == State::Complete) { emit completed(); - emit finished(); + emit finishedChanged(); } } void MarketplaceItemUploader::setError(Error error) { + Q_ASSERT(_state != State::Complete); Q_ASSERT(_error == Error::None); _error = error; emit errorChanged(error); - emit finished(); + emit finishedChanged(); } void MarketplaceItemUploader::send() { @@ -179,7 +182,6 @@ void MarketplaceItemUploader::doUploadAvatar() { { "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"); @@ -197,14 +199,21 @@ void MarketplaceItemUploader::doUploadAvatar() { reply = networkAccessManager.put(request, doc.toJson()); } - connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + connect(reply, &QNetworkReply::uploadProgress, this, [this](float bytesSent, float bytesTotal) { + if (_state == State::UploadingAvatar) { + emit uploadProgress(bytesSent, bytesTotal); + if (bytesSent >= bytesTotal) { + setState(State::WaitingForUploadResponse); + } + } + }); connect(reply, &QNetworkReply::finished, this, [this, reply]() { - //_responseData = reply->readAll(); + _responseData = reply->readAll(); + qWarning() << "Finished request " << _responseData; + auto error = reply->error(); if (error == QNetworkReply::NoError) { - _responseData = reply->readAll(); - qWarning() << "Finished request " << _responseData; auto doc = QJsonDocument::fromJson(_responseData.toLatin1()); auto status = doc.object()["status"].toString(); diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 1b1589af96..4b8b675255 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -21,6 +21,8 @@ class QNetworkReply; class MarketplaceItemUploader : public QObject { Q_OBJECT + Q_PROPERTY(bool finished READ getFinished NOTIFY finishedChanged) + Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged) Q_PROPERTY(State state READ getState NOTIFY stateChanged) Q_PROPERTY(Error error READ getError NOTIFY errorChanged) @@ -38,6 +40,7 @@ public: Idle, GettingCategories, UploadingAvatar, + WaitingForUploadResponse, WaitingForInventory, Complete }; @@ -62,6 +65,7 @@ public: QUuid getMarketplaceID() const { return _marketplaceID; } Error getError() const { return _error; } + bool getFinished() const { return _state == State::Complete || _error != Error::None; } signals: void uploadProgress(qint64 bytesSent, qint64 bytesTotal); @@ -71,7 +75,7 @@ signals: void errorChanged(Error error); // Triggered when the upload has finished, either succesfully completing, or stopping with an error - void finished(); + void finishedChanged(); private: void doGetCategories(); diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index af00428a51..f0e444ba33 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -175,3 +175,8 @@ bool FST::write() { fst.write(FSTReader::writeMapping(getMapping())); return true; } + +void FST::setMarketplaceID(QUuid marketplaceID) { + _marketplaceID = marketplaceID; + emit marketplaceIDChanged(); +} diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 813d4f3bc5..6fd654987e 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -24,6 +24,7 @@ class FST : public QObject { Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameChanged) Q_PROPERTY(QString modelPath READ getModelPath WRITE setModelPath NOTIFY modelPathChanged) Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID) + Q_PROPERTY(bool hasMarketplaceID READ getHasMarketplaceID NOTIFY marketplaceIDChanged) public: FST(const QString& fstPath, QVariantHash data); @@ -37,9 +38,9 @@ public: QString getModelPath() const { return _modelPath; } void setModelPath(const QString& modelPath); - Q_INVOKABLE bool hasMarketplaceID() const { return !_marketplaceID.isNull(); } + Q_INVOKABLE bool getHasMarketplaceID() const { return !_marketplaceID.isNull(); } QUuid getMarketplaceID() const { return _marketplaceID; } - void setMarketplaceID(QUuid marketplaceID) { _marketplaceID = marketplaceID; } + void setMarketplaceID(QUuid marketplaceID); QStringList getScriptPaths() const { return _scriptPaths; } void setScriptPaths(QStringList scriptPaths) { _scriptPaths = scriptPaths; } @@ -53,6 +54,7 @@ public: signals: void nameChanged(const QString& name); void modelPathChanged(const QString& modelPath); + void marketplaceIDChanged(); private: QString _fstPath;