From 4d2d7fa51f5d8cd37d43ff5f245cf03cb41af110 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 22 Feb 2018 13:56:46 -0800 Subject: [PATCH 1/7] lots of fun initial progress! --- interface/resources/fonts/hifi-glyphs.ttf | Bin 32536 -> 32544 bytes .../qml/hifi/commerce/checkout/Checkout.qml | 8 +- .../hifi/commerce/purchases/PurchasedItem.qml | 3 + interface/src/commerce/QmlCommerce.cpp | 99 ++++++++++++++++++ interface/src/commerce/QmlCommerce.h | 8 +- scripts/system/marketplaces/marketplaces.js | 10 +- 6 files changed, 120 insertions(+), 8 deletions(-) diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index 8db0377f88864810e7f893132449ad8e258e68cd..7f7393da1811f3933a2d9c0deabfdbc245a25b47 100644 GIT binary patch delta 216 zcmbR7k8#02Mhyl=1_lORh6V;^h5$FW5Z}t>Df<{0>RA{V7-ZaCT-_M6H9y?UR&-AV_+!z!N9<{xgfu| zq-^uwW(J0eH4F@F`e2I~85p9{zJ$f|+k9ob(8U_Y7h((MH4DuVzTjTj{zA|t#F@V6e#qS@(=uM1=N;&5$a&vHUo3S!) qK3!(X$ZW`9FqyqVkIB$rvw6i-HV*d8yyB9?oSeyb>MXgD^a23O9zII| diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index ab47bb28ad..98f26887ae 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -146,7 +146,8 @@ Rectangle { } onItemTypeChanged: { - if (root.itemType === "entity" || root.itemType === "wearable" || root.itemType === "contentSet" || root.itemType === "avatar") { + if (root.itemType === "entity" || root.itemType === "wearable" || + root.itemType === "contentSet" || root.itemType === "avatar" || root.itemType === "app") { root.isCertified = true; } else { root.isCertified = false; @@ -679,7 +680,7 @@ Rectangle { id: rezNowButton; enabled: (root.itemType === "entity" && root.canRezCertifiedItems) || (root.itemType === "contentSet" && Entities.canReplaceContent()) || - root.itemType === "wearable" || root.itemType === "avatar"; + root.itemType === "wearable" || root.itemType === "avatar" || root.itemType === "app"; buttonGlyph: (root.buttonGlyph)[itemTypesArray.indexOf(root.itemType)]; color: hifi.buttons.red; colorScheme: hifi.colorSchemes.light; @@ -712,6 +713,9 @@ Rectangle { lightboxPopup.button2text = "CONFIRM"; lightboxPopup.button2method = "MyAvatar.useFullAvatarURL('" + root.itemHref + "'); root.visible = false;"; lightboxPopup.visible = true; + } else if (root.itemType === "app") { + // "Run" button is separate. + Commerce.installApp(root.itemHref); } else { sendToScript({method: 'checkout_rezClicked', itemHref: root.itemHref, itemType: root.itemType}); rezzedNotifContainer.visible = true; diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index cc2bcd69aa..c96fc15f5c 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -506,6 +506,9 @@ Item { sendToPurchases({method: 'showReplaceContentLightbox', itemHref: root.itemHref}); } else if (root.itemType === "avatar") { sendToPurchases({method: 'showChangeAvatarLightbox', itemName: root.itemName, itemHref: root.itemHref}); + } else if (root.itemType === "app") { + // "Run" and "Uninstall" buttons are separate. + Commerce.installApp(root.itemHref); } else { sendToPurchases({method: 'purchases_rezClicked', itemHref: root.itemHref, itemType: root.itemType}); root.showConfirmation = true; diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 36c1e422c5..0b583e6153 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -10,6 +10,7 @@ // #include "QmlCommerce.h" +#include "CommerceLogging.h" #include "Application.h" #include "DependencyManager.h" #include "Ledger.h" @@ -17,6 +18,7 @@ #include #include #include +#include QmlCommerce::QmlCommerce() { auto ledger = DependencyManager::get(); @@ -183,3 +185,100 @@ void QmlCommerce::alreadyOwned(const QString& marketplaceId) { auto ledger = DependencyManager::get(); ledger->alreadyOwned(marketplaceId); } + +static QString APP_PATH = PathUtils::getAppDataPath() + "apps"; +bool QmlCommerce::isAppInstalled(const QString& itemHref) { + QUrl appHref(itemHref); + + QFileInfo appFile(APP_PATH + "/" + appHref.fileName()); + if (appFile.exists() && appFile.isFile()) { + return true; + } else { + return false; + } +} + +bool QmlCommerce::installApp(const QString& itemHref) { + if (!QDir(APP_PATH).exists()) { + if (!QDir().mkdir(APP_PATH)) { + qCDebug(commerce) << "Couldn't make APP_PATH directory."; + return false; + } + } + + QUrl appHref(itemHref); + + auto request = + std::unique_ptr(DependencyManager::get()->createResourceRequest(this, appHref)); + + if (!request) { + qCDebug(commerce) << "Couldn't create resource request for app."; + return false; + } + + QEventLoop loop; + connect(request.get(), &ResourceRequest::finished, &loop, &QEventLoop::quit); + request->send(); + loop.exec(); + + if (request->getResult() != ResourceRequest::Success) { + qCDebug(commerce) << "Failed to get .app.json file from remote."; + return false; + } + + // Copy the .app.json to the apps directory inside %AppData%/High Fidelity/Interface + auto requestData = request->getData(); + QFile appFile(APP_PATH + "/" + appHref.fileName()); + if (!appFile.open(QIODevice::WriteOnly)) { + qCDebug(commerce) << "Couldn't open local .app.json file for creation."; + return false; + } + if (appFile.write(requestData) == -1) { + qCDebug(commerce) << "Couldn't write to local .app.json file."; + return false; + } + // Close the file + appFile.close(); + + // Read from the returned datastream to know what .js to add to Running Scripts + QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(requestData); + QJsonObject appFileJsonObject = appFileJsonDocument.object(); + QString scriptUrl = appFileJsonObject["scriptURL"].toString(); + + if ((DependencyManager::get()->loadScript(scriptUrl.trimmed())).isNull()) { + qCDebug(commerce) << "Couldn't load script."; + return false; + } + + emit appInstalled(appHref.fileName()); + return true; +} + +bool QmlCommerce::uninstallApp(const QString& itemHref) { + QUrl appHref(itemHref); + + // Read from the file to know what .js script to stop + QFile appFile(APP_PATH + "/" + appHref.fileName()); + if (!appFile.open(QIODevice::ReadOnly)) { + qCDebug(commerce) << "Couldn't open local .app.json file for deletion."; + return false; + } + QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(appFile.readAll()); + QJsonObject appFileJsonObject = appFileJsonDocument.object(); + QString scriptUrl = appFileJsonObject["scriptURL"].toString(); + + if (!DependencyManager::get()->stopScript(scriptUrl.trimmed(), false)) { + qCDebug(commerce) << "Couldn't stop script."; + return false; + } + + // Delete the .app.json from the filesystem + // remove() closes the file first. + if (!appFile.remove()) { + qCDebug(commerce) << "Couldn't delete local .app.json file."; + return false; + } + + emit appUninstalled(appHref.fileName()); + return true; +} diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index b621608190..60e52a441b 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -51,6 +51,9 @@ signals: void contentSetChanged(const QString& contentSetHref); + void appInstalled(const QString& appFileName); + void appUninstalled(const QString& appFileName); + protected: Q_INVOKABLE void getWalletStatus(); @@ -76,8 +79,11 @@ protected: Q_INVOKABLE void transferHfcToNode(const QString& nodeID, const int& amount, const QString& optionalMessage); Q_INVOKABLE void transferHfcToUsername(const QString& username, const int& amount, const QString& optionalMessage); - Q_INVOKABLE void replaceContentSet(const QString& itemHref); + + Q_INVOKABLE bool isAppInstalled(const QString& itemHref); + Q_INVOKABLE bool installApp(const QString& itemHref); + Q_INVOKABLE bool uninstallApp(const QString& itemHref); }; #endif // hifi_QmlCommerce_h diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 631b5e97ac..ecd1bf2a6e 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -551,11 +551,11 @@ var selectionDisplay = null; // for gridTool.js to ignore break; case 'checkout_rezClicked': case 'purchases_rezClicked': - if (message.itemType === "app") { - console.log("How did you get here? You can't buy apps yet!"); - } else { - rezEntity(message.itemHref, message.itemType); - } + rezEntity(message.itemHref, message.itemType); + break; + case 'checkout_installClicked': + case 'purchases_installClicked': + break; case 'header_marketplaceImageClicked': case 'purchases_backClicked': From de0eee52d6ccaa96d8db21f189d2f0028dd8d33d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 22 Feb 2018 14:45:32 -0800 Subject: [PATCH 2/7] cool --- .../hifi/commerce/purchases/PurchasedItem.qml | 45 +++++++++++++ interface/src/commerce/QmlCommerce.cpp | 67 +++++++++++++++---- interface/src/commerce/QmlCommerce.h | 10 ++- 3 files changed, 107 insertions(+), 15 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index c96fc15f5c..76e2afd308 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -47,6 +47,7 @@ Item { property bool showConfirmation: false; property bool hasPermissionToRezThis; property bool permissionExplanationCardVisible; + property bool isInstalled: false; property string originalStatusText; property string originalStatusColor; @@ -62,6 +63,12 @@ Item { showConfirmation = true; } } + + onAppInstalled: { + if (appHref === root.itemHref) { + root.isInstalled = true; + } + } } Connections { @@ -81,6 +88,10 @@ Item { } else { root.hasPermissionToRezThis = true; } + + if (itemType === "app") { + root.isInstalled = Commerce.isAppInstalled(root.itemHref); + } } onPurchaseStatusChangedChanged: { @@ -472,6 +483,40 @@ Item { } } + Rectangle { + id: appButtonContainer; + color: hifi.colors.white; + z: 994; + visible: root.isInstalled; + anchors.fill: buttonContainer; + + HifiControlsUit.Button { + id: openAppButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.light; + anchors.top: parent.top; + anchors.right: parent.right; + height: 44; + text: "OPEN" + onClicked: { + Commerce.openApp(root.itemHref); + } + } + + HifiControlsUit.Button { + id: uninstallAppButton; + color: hifi.buttons.noneBorderless; + colorScheme: hifi.colorSchemes.light; + anchors.bottom: parent.bottom; + anchors.right: parent.right; + height: 44; + text: "UNINSTALL" + onClicked: { + Commerce.uninstallApp(root.itemHref); + } + } + } + Button { id: buttonContainer; property int color: hifi.buttons.blue; diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 0b583e6153..f9bb0d2003 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include "scripting/HMDScriptingInterface.h" QmlCommerce::QmlCommerce() { auto ledger = DependencyManager::get(); @@ -42,6 +44,8 @@ QmlCommerce::QmlCommerce() { connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() { setPassphrase(""); }); + + _appsPath = PathUtils::getAppDataPath() + "Apps"; } void QmlCommerce::getWalletStatus() { @@ -186,22 +190,32 @@ void QmlCommerce::alreadyOwned(const QString& marketplaceId) { ledger->alreadyOwned(marketplaceId); } -static QString APP_PATH = PathUtils::getAppDataPath() + "apps"; bool QmlCommerce::isAppInstalled(const QString& itemHref) { QUrl appHref(itemHref); - QFileInfo appFile(APP_PATH + "/" + appHref.fileName()); - if (appFile.exists() && appFile.isFile()) { - return true; - } else { + // First check if .app.json exists + QFileInfo appFile(_appsPath + "/" + appHref.fileName()); + if (!(appFile.exists() && appFile.isFile())) { return false; } + + // Then check to see if script is running + auto runningScripts = DependencyManager::get()->getRunningScripts(); + foreach(const QString& runningScript, runningScripts) { + QUrl runningScriptURL = QUrl(runningScript); + qCDebug(commerce) << "ZRF FIXME" << runningScriptURL; + if (runningScriptURL == appHref) { + return true; + } + } + + return false; } bool QmlCommerce::installApp(const QString& itemHref) { - if (!QDir(APP_PATH).exists()) { - if (!QDir().mkdir(APP_PATH)) { - qCDebug(commerce) << "Couldn't make APP_PATH directory."; + if (!QDir(_appsPath).exists()) { + if (!QDir().mkdir(_appsPath)) { + qCDebug(commerce) << "Couldn't make _appsPath directory."; return false; } } @@ -228,7 +242,7 @@ bool QmlCommerce::installApp(const QString& itemHref) { // Copy the .app.json to the apps directory inside %AppData%/High Fidelity/Interface auto requestData = request->getData(); - QFile appFile(APP_PATH + "/" + appHref.fileName()); + QFile appFile(_appsPath + "/" + appHref.fileName()); if (!appFile.open(QIODevice::WriteOnly)) { qCDebug(commerce) << "Couldn't open local .app.json file for creation."; return false; @@ -250,7 +264,7 @@ bool QmlCommerce::installApp(const QString& itemHref) { return false; } - emit appInstalled(appHref.fileName()); + emit appInstalled(itemHref); return true; } @@ -258,7 +272,7 @@ bool QmlCommerce::uninstallApp(const QString& itemHref) { QUrl appHref(itemHref); // Read from the file to know what .js script to stop - QFile appFile(APP_PATH + "/" + appHref.fileName()); + QFile appFile(_appsPath + "/" + appHref.fileName()); if (!appFile.open(QIODevice::ReadOnly)) { qCDebug(commerce) << "Couldn't open local .app.json file for deletion."; return false; @@ -279,6 +293,35 @@ bool QmlCommerce::uninstallApp(const QString& itemHref) { return false; } - emit appUninstalled(appHref.fileName()); + emit appUninstalled(itemHref); + return true; +} + +bool QmlCommerce::openApp(const QString& itemHref) { + QUrl appHref(itemHref); + + // Read from the file to know what .html or .qml document to open + QFile appFile(_appsPath + "/" + appHref.fileName()); + if (!appFile.open(QIODevice::ReadOnly)) { + qCDebug(commerce) << "Couldn't open local .app.json file."; + return false; + } + QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(appFile.readAll()); + QJsonObject appFileJsonObject = appFileJsonDocument.object(); + QString homeUrl = appFileJsonObject["homeURL"].toString(); + + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (homeUrl.contains(".qml", Qt::CaseInsensitive)) { + tablet->loadQMLSource(homeUrl); + } else if (homeUrl.contains(".html", Qt::CaseInsensitive)) { + tablet->gotoWebScreen(homeUrl); + } else { + qCDebug(commerce) << "Attempted to open unknown type of homeURL!"; + return false; + } + + DependencyManager::get()->openTablet(); + return true; } diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 60e52a441b..e0c018878d 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -81,9 +81,13 @@ protected: Q_INVOKABLE void replaceContentSet(const QString& itemHref); - Q_INVOKABLE bool isAppInstalled(const QString& itemHref); - Q_INVOKABLE bool installApp(const QString& itemHref); - Q_INVOKABLE bool uninstallApp(const QString& itemHref); + Q_INVOKABLE bool isAppInstalled(const QString& appHref); + Q_INVOKABLE bool installApp(const QString& appHref); + Q_INVOKABLE bool uninstallApp(const QString& appHref); + Q_INVOKABLE bool openApp(const QString& appHref); + +private: + QString _appsPath; }; #endif // hifi_QmlCommerce_h From 10fa3fa346a2d8a54b650497f421200cdc4ada17 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 22 Feb 2018 16:09:01 -0800 Subject: [PATCH 3/7] It's working! --- .../hifi/commerce/purchases/PurchasedItem.qml | 15 ++++--- .../qml/hifi/commerce/purchases/Purchases.qml | 12 +++++ interface/src/commerce/QmlCommerce.cpp | 44 ++++++++++++------- interface/src/commerce/QmlCommerce.h | 6 +-- scripts/system/commerce/wallet.js | 2 +- 5 files changed, 54 insertions(+), 25 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 76e2afd308..fb8e509cde 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -47,7 +47,7 @@ Item { property bool showConfirmation: false; property bool hasPermissionToRezThis; property bool permissionExplanationCardVisible; - property bool isInstalled: false; + property bool isInstalled; property string originalStatusText; property string originalStatusColor; @@ -69,6 +69,12 @@ Item { root.isInstalled = true; } } + + onAppUninstalled: { + if (appHref === root.itemHref) { + root.isInstalled = false; + } + } } Connections { @@ -88,10 +94,6 @@ Item { } else { root.hasPermissionToRezThis = true; } - - if (itemType === "app") { - root.isInstalled = Commerce.isAppInstalled(root.itemHref); - } } onPurchaseStatusChangedChanged: { @@ -496,6 +498,8 @@ Item { colorScheme: hifi.colorSchemes.light; anchors.top: parent.top; anchors.right: parent.right; + anchors.left: parent.left; + width: 92; height: 44; text: "OPEN" onClicked: { @@ -509,6 +513,7 @@ Item { colorScheme: hifi.colorSchemes.light; anchors.bottom: parent.bottom; anchors.right: parent.right; + anchors.left: parent.left; height: 44; text: "UNINSTALL" onClicked: { diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 9b333a60cd..896200a8e6 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -36,6 +36,7 @@ Rectangle { property bool isShowingMyItems: false; property bool isDebuggingFirstUseTutorial: false; property int pendingItemCount: 0; + property var installedApps; // Style color: hifi.colors.white; Connections { @@ -61,6 +62,8 @@ Rectangle { root.activeView = "firstUseTutorial"; } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { root.activeView = "purchasesMain"; + root.installedApps = Commerce.getInstalledApps(); + console.log("ZRF! " + root.installedApps); Commerce.inventory(); } } else { @@ -269,6 +272,7 @@ Rectangle { case 'tutorial_finished': Settings.setValue("isFirstUseOfPurchases", false); root.activeView = "purchasesMain"; + root.installedApps = Commerce.getInstalledApps(); Commerce.inventory(); break; } @@ -394,6 +398,7 @@ Rectangle { limitedRun: model.limited_run; displayedItemCount: model.displayedItemCount; permissionExplanationCardVisible: model.permissionExplanationCardVisible; + isInstalled: model.isInstalled; itemType: { if (model.root_file_url.indexOf(".fst") > -1) { "avatar"; @@ -680,9 +685,16 @@ Rectangle { if (sameItemCount !== tempPurchasesModel.count || filterBar.text !== filterBar.previousText) { filteredPurchasesModel.clear(); + var currentId; for (var i = 0; i < tempPurchasesModel.count; i++) { + currentId = tempPurchasesModel.get(i).id; + console.log("ZRF HERE 2 " + root.installedApps); + console.log("ZRF HERE 3 " + currentId); + console.log("ZRF HERE 4 " + ((root.installedApps).indexOf(currentId) > -1)); + filteredPurchasesModel.append(tempPurchasesModel.get(i)); filteredPurchasesModel.setProperty(i, 'permissionExplanationCardVisible', false); + filteredPurchasesModel.setProperty(i, 'isInstalled', ((root.installedApps).indexOf(currentId) > -1)); } populateDisplayedItemCounts(); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index f9bb0d2003..f80e88d175 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -45,7 +45,7 @@ QmlCommerce::QmlCommerce() { setPassphrase(""); }); - _appsPath = PathUtils::getAppDataPath() + "Apps"; + _appsPath = PathUtils::getAppDataPath() + "Apps/"; } void QmlCommerce::getWalletStatus() { @@ -190,26 +190,38 @@ void QmlCommerce::alreadyOwned(const QString& marketplaceId) { ledger->alreadyOwned(marketplaceId); } -bool QmlCommerce::isAppInstalled(const QString& itemHref) { - QUrl appHref(itemHref); +QStringList QmlCommerce::getInstalledApps() { + QStringList installedAppsFromMarketplace; + QStringList runningScripts = DependencyManager::get()->getRunningScripts(); - // First check if .app.json exists - QFileInfo appFile(_appsPath + "/" + appHref.fileName()); - if (!(appFile.exists() && appFile.isFile())) { - return false; - } + QDir directory(_appsPath); + qCDebug(commerce) << "ZRF FIXME" << _appsPath; + QStringList apps = directory.entryList(QStringList("*.app.json")); + foreach(QString appFileName, apps) { + installedAppsFromMarketplace.append(appFileName); + qCDebug(commerce) << "ZRF FIXME" << appFileName; + QFile appFile(_appsPath + appFileName); + if (appFile.open(QIODevice::ReadOnly)) { + QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(appFile.readAll()); - // Then check to see if script is running - auto runningScripts = DependencyManager::get()->getRunningScripts(); - foreach(const QString& runningScript, runningScripts) { - QUrl runningScriptURL = QUrl(runningScript); - qCDebug(commerce) << "ZRF FIXME" << runningScriptURL; - if (runningScriptURL == appHref) { - return true; + appFile.close(); + + QJsonObject appFileJsonObject = appFileJsonDocument.object(); + QString scriptURL = appFileJsonObject["scriptURL"].toString(); + + // If the script .app.json is on the user's local disk but the associated script isn't running + // for some reason, start that script again. + if (!runningScripts.contains(scriptURL)) { + if ((DependencyManager::get()->loadScript(scriptURL.trimmed())).isNull()) { + qCDebug(commerce) << "Couldn't start script while checking installed apps."; + } + } + } else { + qCDebug(commerce) << "Couldn't open local .app.json file for reading."; } } - return false; + return installedAppsFromMarketplace; } bool QmlCommerce::installApp(const QString& itemHref) { diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index e0c018878d..2bf4959177 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -51,8 +51,8 @@ signals: void contentSetChanged(const QString& contentSetHref); - void appInstalled(const QString& appFileName); - void appUninstalled(const QString& appFileName); + void appInstalled(const QString& appHref); + void appUninstalled(const QString& appHref); protected: Q_INVOKABLE void getWalletStatus(); @@ -81,7 +81,7 @@ protected: Q_INVOKABLE void replaceContentSet(const QString& itemHref); - Q_INVOKABLE bool isAppInstalled(const QString& appHref); + Q_INVOKABLE QStringList getInstalledApps(); Q_INVOKABLE bool installApp(const QString& appHref); Q_INVOKABLE bool uninstallApp(const QString& appHref); Q_INVOKABLE bool openApp(const QString& appHref); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 8cf5b72b9a..9ff7038c09 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -39,7 +39,7 @@ // for toolbar-mode: go back to home screen, this will close the window. tablet.gotoHomeScreen(); } else { - tablet.loadQMLSource(WALLET_QML_SOURCE); + tablet.loadQMLSource(MARKETPLACE_PURCHASES_QML_PATH); } } From 33f73fef0df20528ca3867a41d4a1257fc4791f9 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 22 Feb 2018 16:26:35 -0800 Subject: [PATCH 4/7] YAY! --- .../resources/qml/hifi/commerce/purchases/Purchases.qml | 6 +----- interface/src/commerce/QmlCommerce.cpp | 9 ++++----- interface/src/commerce/QmlCommerce.h | 2 +- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 896200a8e6..3612de7323 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -36,7 +36,7 @@ Rectangle { property bool isShowingMyItems: false; property bool isDebuggingFirstUseTutorial: false; property int pendingItemCount: 0; - property var installedApps; + property string installedApps; // Style color: hifi.colors.white; Connections { @@ -63,7 +63,6 @@ Rectangle { } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { root.activeView = "purchasesMain"; root.installedApps = Commerce.getInstalledApps(); - console.log("ZRF! " + root.installedApps); Commerce.inventory(); } } else { @@ -688,9 +687,6 @@ Rectangle { var currentId; for (var i = 0; i < tempPurchasesModel.count; i++) { currentId = tempPurchasesModel.get(i).id; - console.log("ZRF HERE 2 " + root.installedApps); - console.log("ZRF HERE 3 " + currentId); - console.log("ZRF HERE 4 " + ((root.installedApps).indexOf(currentId) > -1)); filteredPurchasesModel.append(tempPurchasesModel.get(i)); filteredPurchasesModel.setProperty(i, 'permissionExplanationCardVisible', false); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index f80e88d175..6de5de1a9d 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -190,16 +190,15 @@ void QmlCommerce::alreadyOwned(const QString& marketplaceId) { ledger->alreadyOwned(marketplaceId); } -QStringList QmlCommerce::getInstalledApps() { - QStringList installedAppsFromMarketplace; +QString QmlCommerce::getInstalledApps() { + QString installedAppsFromMarketplace; QStringList runningScripts = DependencyManager::get()->getRunningScripts(); QDir directory(_appsPath); - qCDebug(commerce) << "ZRF FIXME" << _appsPath; QStringList apps = directory.entryList(QStringList("*.app.json")); foreach(QString appFileName, apps) { - installedAppsFromMarketplace.append(appFileName); - qCDebug(commerce) << "ZRF FIXME" << appFileName; + installedAppsFromMarketplace += appFileName; + installedAppsFromMarketplace += ","; QFile appFile(_appsPath + appFileName); if (appFile.open(QIODevice::ReadOnly)) { QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(appFile.readAll()); diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 2bf4959177..09eb7137af 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -81,7 +81,7 @@ protected: Q_INVOKABLE void replaceContentSet(const QString& itemHref); - Q_INVOKABLE QStringList getInstalledApps(); + Q_INVOKABLE QString getInstalledApps(); Q_INVOKABLE bool installApp(const QString& appHref); Q_INVOKABLE bool uninstallApp(const QString& appHref); Q_INVOKABLE bool openApp(const QString& appHref); From f14673bc5079622d019919f004bb54e4539768d9 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 22 Feb 2018 16:37:12 -0800 Subject: [PATCH 5/7] Fixup Checkout for apps --- .../qml/hifi/commerce/checkout/Checkout.qml | 16 +++++++++++++--- scripts/system/commerce/wallet.js | 2 +- scripts/system/marketplaces/marketplaces.js | 4 ---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 98f26887ae..10894109ef 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -48,6 +48,7 @@ Rectangle { property bool debugCheckoutSuccess: false; property bool canRezCertifiedItems: Entities.canRezCertified() || Entities.canRezTmpCertified(); property string referrer; + property bool isInstalled; // Style color: hifi.colors.white; Connections { @@ -122,6 +123,12 @@ Rectangle { root.refreshBuyUI(); } } + + onAppInstalled: { + if (appHref === root.itemHref) { + root.isInstalled = true; + } + } } onItemIdChanged: { @@ -689,7 +696,7 @@ Rectangle { height: 50; anchors.left: parent.left; anchors.right: parent.right; - text: (root.buttonTextNormal)[itemTypesArray.indexOf(root.itemType)]; + text: root.itemType === "app" && root.isInstalled ? "OPEN APP" : (root.buttonTextNormal)[itemTypesArray.indexOf(root.itemType)]; onClicked: { if (root.itemType === "contentSet") { lightboxPopup.titleText = "Replace Content"; @@ -714,8 +721,11 @@ Rectangle { lightboxPopup.button2method = "MyAvatar.useFullAvatarURL('" + root.itemHref + "'); root.visible = false;"; lightboxPopup.visible = true; } else if (root.itemType === "app") { - // "Run" button is separate. - Commerce.installApp(root.itemHref); + if (root.isInstalled) { + Commerce.openApp(root.itemHref); + } else { + Commerce.installApp(root.itemHref); + } } else { sendToScript({method: 'checkout_rezClicked', itemHref: root.itemHref, itemType: root.itemType}); rezzedNotifContainer.visible = true; diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 9ff7038c09..8cf5b72b9a 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -39,7 +39,7 @@ // for toolbar-mode: go back to home screen, this will close the window. tablet.gotoHomeScreen(); } else { - tablet.loadQMLSource(MARKETPLACE_PURCHASES_QML_PATH); + tablet.loadQMLSource(WALLET_QML_SOURCE); } } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index ecd1bf2a6e..8f51d88f2d 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -552,10 +552,6 @@ var selectionDisplay = null; // for gridTool.js to ignore case 'checkout_rezClicked': case 'purchases_rezClicked': rezEntity(message.itemHref, message.itemType); - break; - case 'checkout_installClicked': - case 'purchases_installClicked': - break; case 'header_marketplaceImageClicked': case 'purchases_backClicked': From 249f0568a14157e8a48f0071b65283127e9f93d1 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 22 Feb 2018 17:14:23 -0800 Subject: [PATCH 6/7] New loader --- .../qml/hifi/commerce/checkout/Checkout.qml | 7 ++++--- .../hifi/commerce/common/images/loader-blue.gif | Bin 0 -> 59768 bytes 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 interface/resources/qml/hifi/commerce/common/images/loader-blue.gif diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 10894109ef..d88ded6a15 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -319,7 +319,7 @@ Rectangle { z: 997; visible: !root.ownershipStatusReceived || !root.balanceReceived; anchors.fill: parent; - color: Qt.rgba(0.0, 0.0, 0.0, 0.7); + color: hifi.colors.white; // This object is always used in a popup. // This MouseArea is used to prevent a user from being @@ -331,8 +331,9 @@ Rectangle { } AnimatedImage { - source: "../common/images/loader.gif" - width: 96; + id: loadingImage; + source: "../common/images/loader-blue.gif" + width: 74; height: width; anchors.verticalCenter: parent.verticalCenter; anchors.horizontalCenter: parent.horizontalCenter; diff --git a/interface/resources/qml/hifi/commerce/common/images/loader-blue.gif b/interface/resources/qml/hifi/commerce/common/images/loader-blue.gif new file mode 100644 index 0000000000000000000000000000000000000000..8b9e17053b2420e2ea3dcf68e3677d172858a9b5 GIT binary patch literal 59768 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 Date: Fri, 23 Feb 2018 10:19:00 -0800 Subject: [PATCH 7/7] Don't use eventLoop, since that will block the UI --- interface/src/commerce/QmlCommerce.cpp | 70 +++++++++++++------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 6de5de1a9d..e7d62930cf 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -233,49 +233,47 @@ bool QmlCommerce::installApp(const QString& itemHref) { QUrl appHref(itemHref); - auto request = - std::unique_ptr(DependencyManager::get()->createResourceRequest(this, appHref)); + auto request = DependencyManager::get()->createResourceRequest(this, appHref); if (!request) { qCDebug(commerce) << "Couldn't create resource request for app."; return false; } - QEventLoop loop; - connect(request.get(), &ResourceRequest::finished, &loop, &QEventLoop::quit); + connect(request, &ResourceRequest::finished, this, [=]() { + if (request->getResult() != ResourceRequest::Success) { + qCDebug(commerce) << "Failed to get .app.json file from remote."; + return false; + } + + // Copy the .app.json to the apps directory inside %AppData%/High Fidelity/Interface + auto requestData = request->getData(); + QFile appFile(_appsPath + "/" + appHref.fileName()); + if (!appFile.open(QIODevice::WriteOnly)) { + qCDebug(commerce) << "Couldn't open local .app.json file for creation."; + return false; + } + if (appFile.write(requestData) == -1) { + qCDebug(commerce) << "Couldn't write to local .app.json file."; + return false; + } + // Close the file + appFile.close(); + + // Read from the returned datastream to know what .js to add to Running Scripts + QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(requestData); + QJsonObject appFileJsonObject = appFileJsonDocument.object(); + QString scriptUrl = appFileJsonObject["scriptURL"].toString(); + + if ((DependencyManager::get()->loadScript(scriptUrl.trimmed())).isNull()) { + qCDebug(commerce) << "Couldn't load script."; + return false; + } + + emit appInstalled(itemHref); + return true; + }); request->send(); - loop.exec(); - - if (request->getResult() != ResourceRequest::Success) { - qCDebug(commerce) << "Failed to get .app.json file from remote."; - return false; - } - - // Copy the .app.json to the apps directory inside %AppData%/High Fidelity/Interface - auto requestData = request->getData(); - QFile appFile(_appsPath + "/" + appHref.fileName()); - if (!appFile.open(QIODevice::WriteOnly)) { - qCDebug(commerce) << "Couldn't open local .app.json file for creation."; - return false; - } - if (appFile.write(requestData) == -1) { - qCDebug(commerce) << "Couldn't write to local .app.json file."; - return false; - } - // Close the file - appFile.close(); - - // Read from the returned datastream to know what .js to add to Running Scripts - QJsonDocument appFileJsonDocument = QJsonDocument::fromJson(requestData); - QJsonObject appFileJsonObject = appFileJsonDocument.object(); - QString scriptUrl = appFileJsonObject["scriptURL"].toString(); - - if ((DependencyManager::get()->loadScript(scriptUrl.trimmed())).isNull()) { - qCDebug(commerce) << "Couldn't load script."; - return false; - } - - emit appInstalled(itemHref); return true; }