diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 59fa66af0b..0b34a8f9ac 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -17,7 +17,7 @@ import QtGraphicalEffects 1.0 import "toolbars" import "../styles-uit" -Rectangle { +Item { id: root; property string userName: ""; property string placeName: ""; @@ -45,7 +45,7 @@ Rectangle { property int textSizeSmall: 18; property int stackShadowNarrowing: 5; property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif"); - property int shadowHeight: 20; + property int shadowHeight: 10; HifiConstants { id: hifi } function pastTime(timestamp) { // Answer a descriptive string @@ -70,6 +70,40 @@ Rectangle { } property bool hasGif: imageUrl.indexOf('.gif') === (imageUrl.length - 4); + + DropShadow { + visible: isStacked; + anchors.fill: shadow1; + source: shadow1; + verticalOffset: 2; + radius: 4; + samples: 9; + color: hifi.colors.baseGrayShadow; + } + Rectangle { + id: shadow1; + visible: isStacked; + width: parent.width - stackShadowNarrowing; + height: shadowHeight; + anchors { + top: parent.bottom; + horizontalCenter: parent.horizontalCenter; + } + } + DropShadow { + anchors.fill: base; + source: base; + verticalOffset: 2; + radius: 4; + samples: 9; + color: hifi.colors.baseGrayShadow; + } + Rectangle { + id: base; + color: "white"; + anchors.fill: parent; + } + AnimatedImage { id: animation; // Always visible, to drive loading, but initially covered up by lobby during load. @@ -96,34 +130,6 @@ Rectangle { } } } - Rectangle { - id: shadow1; - visible: isStacked; - width: parent.width - stackShadowNarrowing; - height: shadowHeight / 2; - anchors { - top: parent.bottom; - horizontalCenter: parent.horizontalCenter; - } - gradient: Gradient { - GradientStop { position: 0.0; color: "gray" } - GradientStop { position: 1.0; color: "white" } - } - } - Rectangle { - id: shadow2; - visible: isStacked; - width: shadow1.width - stackShadowNarrowing; - height: shadowHeight / 2; - anchors { - top: shadow1.bottom; - horizontalCenter: parent.horizontalCenter; - } - gradient: Gradient { - GradientStop { position: 0.0; color: "gray" } - GradientStop { position: 1.0; color: "white" } - } - } property int dropHorizontalOffset: 0; property int dropVerticalOffset: 1; property int dropRadius: 2; @@ -168,7 +174,7 @@ Rectangle { source: "../../images/snap-icon.svg" width: 40; height: 40; - visible: action === 'snapshot'; + visible: (action === 'snapshot') && (messageHeight >= 40); } RalewayRegular { id: message; @@ -209,7 +215,7 @@ Rectangle { StateImage { id: actionIcon; imageURL: "../../images/info-icon-2-state.svg"; - size: 32; + size: 30; buttonState: messageArea.containsMouse ? 1 : 0; anchors { bottom: parent.bottom; diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index d95518b891..b03144644a 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -23,7 +23,15 @@ Column { property int cardWidth: 212; property int cardHeight: 152; - property int stackedCardShadowHeight: 10; + property int textPadding: 10; + property int smallMargin: 4; + property int messageHeight: 40; + property int textSize: 24; + property int textSizeSmall: 18; + property int stackShadowNarrowing: 5; + property int stackedCardShadowHeight: 4; + property int labelSize: 20; + property string metaverseServerUrl: ''; property string actions: 'snapshot'; onActionsChanged: fillDestinations(); @@ -118,7 +126,7 @@ Column { } function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches var words = filter.toUpperCase().split(/\s+/).filter(identity); - function suggestable(story) { // fixme add to makeFilteredStoryProcessor + function suggestable(story) { if (story.action === 'snapshot') { return true; } @@ -161,25 +169,26 @@ Column { root.visible = !!suggestions.count; } - RalewayLight { + RalewayBold { id: label; text: labelText; - color: hifi.colors.white; - size: 28; + color: hifi.colors.blueAccent; + size: labelSize; } ListView { id: scroll; - clip: true; model: suggestions; orientation: ListView.Horizontal; highlightMoveDuration: -1; highlightMoveVelocity: -1; - highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.blueHighlight; z: 1; } + highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.primaryHighlight; z: 1; } + currentIndex: -1; - spacing: 14; + spacing: 12; width: parent.width; height: cardHeight + stackedCardShadowHeight; delegate: Card { + id: card; width: cardWidth; height: cardHeight; goFunction: root.goFunction; @@ -193,7 +202,15 @@ Column { onlineUsers: model.online_users; storyId: model.metaverseId; drillDownToPlace: model.drillDownToPlace; - shadowHeight: stackedCardShadowHeight; + + textPadding: root.textPadding; + smallMargin: root.smallMargin; + messageHeight: root.messageHeight; + textSize: root.textSize; + textSizeSmall: root.textSizeSmall; + stackShadowNarrowing: root.stackShadowNarrowing; + shadowHeight: root.stackedCardShadowHeight; + hoverThunk: function () { scroll.currentIndex = index; } unhoverThunk: function () { scroll.currentIndex = -1; } } diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 5578b94168..9689583649 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -154,7 +154,7 @@ StackView { left: parent.left; } - HifiStyles.RalewayLight { + HifiStyles.RalewayRegular { id: notice; font.pixelSize: hifi.fonts.pixelSize * 0.7; anchors { @@ -224,63 +224,73 @@ StackView { Rectangle { id: bgMain; - color: hifiStyleConstants.colors.faintGray50; anchors { top: addressBar.bottom; bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom; left: parent.left; right: parent.right; } - ScrollView { - anchors.fill: bgMain; - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff; - verticalScrollBarPolicy: Qt.ScrollBarAsNeeded; - Rectangle { // Column margins require QtQuick 2.7, which we don't use yet. - id: column; - property real pad: 10; - width: bgMain.width - column.pad; - height: stack.height; - color: "transparent"; - anchors { - left: parent.left; - leftMargin: column.pad; - topMargin: column.pad; + Rectangle { + id: addressShadow; + width: parent.width; + height: 42 - 33; + gradient: Gradient { + GradientStop { position: 0.0; color: "gray" } + GradientStop { position: 1.0; color: "white" } + } + } + Rectangle { // Column margins require QtQuick 2.7, which we don't use yet. + id: column; + property real pad: 10; + width: bgMain.width - column.pad; + height: stack.height; + color: "transparent"; + anchors { + left: parent.left; + leftMargin: column.pad; + top: addressShadow.bottom; + topMargin: column.pad; + } + Column { + id: stack; + width: column.width; + spacing: 33 - places.labelSize; + Feed { + id: happeningNow; + width: parent.width; + cardWidth: 312 + (2 * 4); + cardHeight: 163 + (2 * 4); + metaverseServerUrl: addressBarDialog.metaverseServerUrl; + labelText: 'HAPPENING NOW'; + //actions: 'concurrency,snapshot'; // uncomment this line instead of next to produce fake announcement data for testing. + actions: 'announcement'; + filter: addressLine.text; + goFunction: goCard; } - Column { - id: stack; - width: column.width; - spacing: column.pad; - Feed { - id: happeningNow; - width: parent.width; - property real cardScale: 1.5; - cardWidth: places.cardWidth * happeningNow.cardScale; - cardHeight: places.cardHeight * happeningNow.cardScale; - metaverseServerUrl: addressBarDialog.metaverseServerUrl; - labelText: 'Happening Now'; - //actions: 'concurrency,snapshot'; // uncomment this line instead of next to produce fake announcement data for testing. - actions: 'announcement'; - filter: addressLine.text; - goFunction: goCard; - } - Feed { - id: places; - width: parent.width; - metaverseServerUrl: addressBarDialog.metaverseServerUrl; - labelText: 'Places'; - actions: 'concurrency'; - filter: addressLine.text; - goFunction: goCard; - } - Feed { - id: snapshots; - width: parent.width; - metaverseServerUrl: addressBarDialog.metaverseServerUrl; - labelText: 'Recent Activity'; - actions: 'snapshot'; - filter: addressLine.text; - goFunction: goCard; - } + Feed { + id: places; + width: parent.width; + cardWidth: 210; + cardHeight: 110 + messageHeight; + messageHeight: 44; + metaverseServerUrl: addressBarDialog.metaverseServerUrl; + labelText: 'PLACES'; + actions: 'concurrency'; + filter: addressLine.text; + goFunction: goCard; + } + Feed { + id: snapshots; + width: parent.width; + cardWidth: 143 + (2 * 4); + cardHeight: 75 + messageHeight + 4; + messageHeight: 32; + textPadding: 6; + metaverseServerUrl: addressBarDialog.metaverseServerUrl; + labelText: 'RECENT SNAPS'; + actions: 'snapshot'; + filter: addressLine.text; + goFunction: goCard; } } } @@ -369,8 +379,8 @@ StackView { notice.text = "Go To a place, @user, path, or network address:"; notice.color = hifiStyleConstants.colors.baseGrayHighlight; } else { - notice.text = AddressManager.isConnected ? "Your location:" : "Not Connected"; - notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.baseGrayHighlight : hifiStyleConstants.colors.redHighlight; + notice.text = AddressManager.isConnected ? "YOUR LOCATION" : "NOT CONNECTED"; + notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.blueHighlight : hifiStyleConstants.colors.redHighlight; // Display hostname, which includes ip address, localhost, and other non-placenames. location.text = (AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : ''); } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9e437e5d26..e8fe082d10 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -535,6 +535,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set<OctreeStatsProvider>(nullptr, qApp->getOcteeSceneStats()); DependencyManager::set<AvatarBookmarks>(); DependencyManager::set<LocationBookmarks>(); + DependencyManager::set<Snapshot>(); return previousSessionCrashed; } @@ -2052,6 +2053,7 @@ void Application::initializeUi() { rootContext->setContextProperty("Scene", DependencyManager::get<SceneScriptingInterface>().data()); rootContext->setContextProperty("Render", _renderEngine->getConfiguration().get()); rootContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); + rootContext->setContextProperty("Snapshot", DependencyManager::get<Snapshot>().data()); rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); @@ -5517,6 +5519,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get<Snapshot>().data()); scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get<AudioClient>()->getStats().data()); scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get<AudioScope>().data()); @@ -6461,7 +6464,7 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa // Get a screenshot and save it QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); // If we're not doing an animated snapshot as well... - if (!includeAnimated || !(SnapshotAnimated::alsoTakeAnimatedSnapshot.get())) { + if (!includeAnimated) { // Tell the dependency manager that the capture of the still snapshot has taken place. emit DependencyManager::get<WindowScriptingInterface>()->stillSnapshotTaken(path, notify); } else { diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 2df825d643..1e14c24da3 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -168,6 +168,28 @@ void WindowScriptingInterface::ensureReticleVisible() const { } } +/// Display a "browse to directory" dialog. If `directory` is an invalid file or directory the browser will start at the current +/// working directory. +/// \param const QString& title title of the window +/// \param const QString& directory directory to start the file browser at +/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog` +/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue` +QScriptValue WindowScriptingInterface::browseDir(const QString& title, const QString& directory) { + ensureReticleVisible(); + QString path = directory; + if (path.isEmpty()) { + path = getPreviousBrowseLocation(); + } +#ifndef Q_OS_WIN + path = fixupPathForMac(directory); +#endif + QString result = OffscreenUi::getExistingDirectory(nullptr, title, path); + if (!result.isEmpty()) { + setPreviousBrowseLocation(QFileInfo(result).absolutePath()); + } + return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); +} + /// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current /// working directory. /// \param const QString& title title of the window diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index eb7dc02627..2b1e48d918 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -51,6 +51,7 @@ public slots: QScriptValue confirm(const QString& message = ""); QScriptValue prompt(const QString& message = "", const QString& defaultText = ""); CustomPromptResult customPrompt(const QVariant& config); + QScriptValue browseDir(const QString& title = "", const QString& directory = ""); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); @@ -74,7 +75,7 @@ signals: void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo); void stillSnapshotTaken(const QString& pathStillSnapshot, bool notify); - void snapshotShared(const QString& error); + void snapshotShared(bool isError, const QString& reply); void processingGifStarted(const QString& pathStillSnapshot); void processingGifCompleted(const QString& pathAnimatedSnapshot); diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index a12d9020ae..617ac1ed1c 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -116,11 +116,6 @@ void setupPreferences() { auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } - { - auto getter = []()->bool { return SnapshotAnimated::alsoTakeAnimatedSnapshot.get(); }; - auto setter = [](bool value) { SnapshotAnimated::alsoTakeAnimatedSnapshot.set(value); }; - preferences->addPreference(new CheckPreference(SNAPSHOTS, "Take Animated GIF Snapshot", getter, setter)); - } { auto getter = []()->float { return SnapshotAnimated::snapshotAnimatedDuration.get(); }; auto setter = [](float value) { SnapshotAnimated::snapshotAnimatedDuration.set(value); }; diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index f75190530c..59ecce5bc7 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -194,3 +194,10 @@ void Snapshot::uploadSnapshot(const QString& filename, const QUrl& href) { multiPart); } +QString Snapshot::getSnapshotsLocation() { + return snapshotsLocation.get(""); +} + +void Snapshot::setSnapshotsLocation(const QString& location) { + snapshotsLocation.set(location); +} diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 14e1bc2e9f..93ffbbc7bb 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -18,6 +18,7 @@ #include <QStandardPaths> #include <SettingHandle.h> +#include <DependencyManager.h> class QFile; class QTemporaryFile; @@ -32,7 +33,9 @@ private: QUrl _URL; }; -class Snapshot { +class Snapshot : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY public: static QString saveSnapshot(QImage image); static QTemporaryFile* saveTempSnapshot(QImage image); @@ -40,6 +43,10 @@ public: static Setting::Handle<QString> snapshotsLocation; static void uploadSnapshot(const QString& filename, const QUrl& href = QUrl("")); + +public slots: + Q_INVOKABLE QString getSnapshotsLocation(); + Q_INVOKABLE void setSnapshotsLocation(const QString& location); private: static QFile* savedFileForSnapshot(QImage & image, bool isTemporary); }; diff --git a/interface/src/ui/SnapshotUploader.cpp b/interface/src/ui/SnapshotUploader.cpp index 411e892de5..aa37608476 100644 --- a/interface/src/ui/SnapshotUploader.cpp +++ b/interface/src/ui/SnapshotUploader.cpp @@ -49,6 +49,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { userStoryObject.insert("place_name", placeName); userStoryObject.insert("path", currentPath); userStoryObject.insert("action", "snapshot"); + userStoryObject.insert("audience", "for_url"); rootObject.insert("user_story", userStoryObject); auto accountManager = DependencyManager::get<AccountManager>(); @@ -61,7 +62,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QJsonDocument(rootObject).toJson()); } else { - emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(contents); + emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(true, contents); delete this; } } @@ -72,12 +73,13 @@ void SnapshotUploader::uploadFailure(QNetworkReply& reply) { if (replyString.size() == 0) { replyString = reply.errorString(); } - emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(replyString); // maybe someday include _inWorldLocation, _filename? + emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(true, replyString); // maybe someday include _inWorldLocation, _filename? delete this; } void SnapshotUploader::createStorySuccess(QNetworkReply& reply) { - emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(QString()); + QString replyString = reply.readAll(); + emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(false, replyString); delete this; } @@ -87,7 +89,7 @@ void SnapshotUploader::createStoryFailure(QNetworkReply& reply) { if (replyString.size() == 0) { replyString = reply.errorString(); } - emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(replyString); + emit DependencyManager::get<WindowScriptingInterface>()->snapshotShared(true, replyString); delete this; } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 24b12dacf0..fedead5aa5 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -51,6 +51,7 @@ #include "ui/AvatarInputs.h" #include "avatar/AvatarManager.h" #include "scripting/GlobalServicesScriptingInterface.h" +#include "ui/Snapshot.h" static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; @@ -177,6 +178,7 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("Quat", new Quat()); _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get<AvatarManager>()->getMyAvatar().get()); _webSurface->getRootContext()->setContextProperty("Entities", DependencyManager::get<EntityScriptingInterface>().data()); + _webSurface->getRootContext()->setContextProperty("Snapshot", DependencyManager::get<Snapshot>().data()); if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>(); diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index b36abebb1b..6edd969568 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -568,8 +568,10 @@ void AnimInverseKinematics::computeHipsOffset(const std::vector<IKTarget>& targe newHipsOffset += targetPosition - actualPosition; // Add downward pressure on the hips - newHipsOffset *= 0.95f; - newHipsOffset -= 1.0f; + const float PRESSURE_SCALE_FACTOR = 0.95f; + const float PRESSURE_TRANSLATION_OFFSET = 1.0f; + newHipsOffset *= PRESSURE_SCALE_FACTOR; + newHipsOffset -= PRESSURE_TRANSLATION_OFFSET; } } else if (target.getType() == IKTarget::Type::RotationAndPosition) { glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); @@ -627,7 +629,7 @@ static void setEllipticalSwingLimits(SwingTwistConstraint* stConstraint, float l const int NUM_SUBDIVISIONS = 8; std::vector<float> minDots; minDots.reserve(NUM_SUBDIVISIONS); - float dTheta = (2.0f * PI) / NUM_SUBDIVISIONS; + float dTheta = TWO_PI / NUM_SUBDIVISIONS; float theta = 0.0f; for (int i = 0; i < NUM_SUBDIVISIONS; i++) { minDots.push_back(cosf(glm::length(glm::vec2(anteriorSwingTheta * cosf(theta), lateralSwingTheta * sinf(theta))))); @@ -829,7 +831,9 @@ void AnimInverseKinematics::initConstraints() { stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST); // limit lateral swings more then forward-backward swings - setEllipticalSwingLimits(stConstraint, PI / 30.0f, PI / 20.0f); + const float MAX_SPINE_LATERAL_SWING = PI / 30.0f; + const float MAX_SPINE_ANTERIOR_SWING = PI / 20.0f; + setEllipticalSwingLimits(stConstraint, MAX_SPINE_LATERAL_SWING, MAX_SPINE_ANTERIOR_SWING); if (0 == baseName.compare("Spine1", Qt::CaseSensitive) || 0 == baseName.compare("Spine", Qt::CaseSensitive)) { @@ -844,7 +848,10 @@ void AnimInverseKinematics::initConstraints() { const float MAX_NECK_TWIST = PI / 10.0f; stConstraint->setTwistLimits(-MAX_NECK_TWIST, MAX_NECK_TWIST); - setEllipticalSwingLimits(stConstraint, PI / 10.0f, PI / 8.0f); + // limit lateral swings more then forward-backward swings + const float MAX_NECK_LATERAL_SWING = PI / 10.0f; + const float MAX_NECK_ANTERIOR_SWING = PI / 8.0f; + setEllipticalSwingLimits(stConstraint, MAX_NECK_LATERAL_SWING, MAX_NECK_ANTERIOR_SWING); constraint = static_cast<RotationConstraint*>(stConstraint); } else if (0 == baseName.compare("Head", Qt::CaseSensitive)) { diff --git a/scripts/system/html/SnapshotReview.html b/scripts/system/html/SnapshotReview.html index 145cfb16a9..fb40c04d05 100644 --- a/scripts/system/html/SnapshotReview.html +++ b/scripts/system/html/SnapshotReview.html @@ -1,15 +1,17 @@ <html> <head> <title>Share</title> - <link rel="stylesheet" type="text/css" href="css/edit-style.css"> + <link rel="stylesheet" type="text/css" href="css/hifi-style.css"> <link rel="stylesheet" type="text/css" href="css/SnapshotReview.css"> <script type="text/javascript" src="js/eventBridgeLoader.js"></script> <script type="text/javascript" src="js/SnapshotReview.js"></script> </head> <body> - <div class="snapsection title"> - <label>Snap</label> + <div class="title"> + <label>Snapshots</label> + <label id="settingsLabel" for="snapshotSettings">Settings</label> + <input type="button" class="hifi-glyph naked" id="snapshotSettings" value="@" onclick="snapshotSettings()" /> </div> <hr /> <div id="snapshot-pane"> @@ -17,30 +19,16 @@ </div> </div> <div id="snapshot-controls"> - <div class="snapsection" id="snap-buttons"> - <div id="sharing"> - <div class="button"> - <span class="compound-button"> - <input type="button" class="blue" id="share" value="Share in Feed" onclick="shareSelected()" /> - <span class="glyph"></span> - </span> - </div> - </div> - <div class="button"> - <input type="button" class="black" id="close" value="Don't Share" onclick="doNotShare()" /> - </div> - </div> - <hr /> - <div class="snapsection" id="snap-settings"> - <span class="setting"> - <input type="button" class="glyph naked" id="snapshotSettings" value="@" onclick="snapshotSettings()" /> - <label for="snapshotSettings">Settings</label> - </span> - <span class="setting checkbox"> - <input id="openFeed" type="checkbox" checked /> - <label for="openFeed">Open feed after</label> - </span> + <div id="snap-settings"> + <label>CAMERA CAPTURES</label><br /> + <form action=""> + <input type="radio" name="cameraCaptures" id="stillAndGif" value="stillAndGif" /><label for="stillAndGif"><span><span></span></span>Still + GIF</label> + <br /> + <input type="radio" name="cameraCaptures" id="stillOnly" value="stillOnly" /><label for="stillOnly"><span><span></span></span>Still Only</label> + </form> </div> + <input type="button" id="snap-button" onclick="takeSnapshot()" /> + <div id="snap-settings-right"></div> </div> </body> </html> diff --git a/scripts/system/html/css/SnapshotReview.css b/scripts/system/html/css/SnapshotReview.css index 34b690a021..12b91d372b 100644 --- a/scripts/system/html/css/SnapshotReview.css +++ b/scripts/system/html/css/SnapshotReview.css @@ -8,142 +8,280 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html */ -body { - padding-top: 0; - padding-bottom: 14px; -} +/* +// START styling of top bar and its contents +*/ -.snapsection { - padding-top: 14px; - text-align: center; -} - -.snapsection.title { - padding-top: 0; +.title { + padding: 6px 10px; text-align: left; + height: 26px; + line-height: 26px; + clear: both; } .title label { - font-size: 18px; position: relative; - top: 12px; + font-size: 18px; + float: left; } +#snapshotSettings { + position: relative; + float: right; +} +#settingsLabel { + position: relative; + float: right; + font-family: Raleway-SemiBold; + font-size: 14px; +} +.hifi-glyph { + font-size: 30px; + top: -4px; +} +input[type=button].naked { + color: #afafaf; + background: none; +} +input[type=button].naked:hover { + color: #ffffff; +} +input[type=button].naked:active { + color: #afafaf; +} +/* +// END styling of top bar and its contents +*/ + +/* +// START styling of snapshot instructions panel +*/ +.snapshotInstructions { + font-family: Raleway-Regular; + margin: 0 20px; + width: 100%; + height: 50%; +} +/* +// END styling of snapshot instructions panel +*/ + +/* +// START styling of snapshot pane and its contents +*/ #snapshot-pane { width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - box-sizing: border-box; - padding-top: 56px; - padding-bottom: 175px; + height: 560px; + display: flex; + justify-content: center; + align-items: center; } #snapshot-images { - height: 100%; width: 100%; - position: relative; -} - -#snapshot-images > div { - position: relative; - text-align: center; } #snapshot-images img { max-width: 100%; max-height: 100%; +} + +.gifLabel { + position:absolute; + left: 15px; + top: 10px; + font-family: Raleway-SemiBold; + font-size: 18px; + color: white; + text-shadow: 2px 2px 3px #000000; +} +/* +// END styling of snapshot pane and its contents +*/ + +/* +// START styling of share bar +*/ +.shareControls { + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: center; + height: 50px; + line-height: 60px; + width: calc(100% - 8px); position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - vertical-align: middle; + bottom: 4px; + left: 4px; + right: 4px; } - -#snapshot-images div.property { - margin-top: 0; +.shareButtons { + display: flex; + align-items: center; + margin-left: 30px; + height: 100%; + width: 80%; +} +.blastToConnections { + text-align: left; + margin-right: 25px; + height: 29px; +} +.shareWithEveryone { + background: #DDDDDD url(../img/shareToFeed.png) no-repeat scroll center; + border-width: 0px; + text-align: left; + margin-right: 8px; + height: 29px; + width: 30px; + border-radius: 3px; +} +.facebookButton { + background-image: url(../img/fb_logo.png); + width: 29px; + height: 29px; + display: inline-block; + margin-right: 8px; +} +.twitterButton { + background-image: url(../img/twitter_logo.png); + width: 29px; + height: 29px; + display: inline-block; + margin-right: 8px; + border-radius: 3px; +} +.showShareButtonsButtonDiv { + display: inline-flex; + align-items: center; + font-family: Raleway-SemiBold; + font-size: 14px; + color: white; + text-shadow: 2px 2px 3px #000000; + height: 100%; + margin-right: 10px; + width: 20%; +} +.showShareButton { + width: 40px; + height: 40px; + border-radius: 50%; + border-width: 0; + margin-left: 5px; + outline: none; +} +.showShareButton.active { + border-color: #00b4ef; + border-width: 3px; + background-color: white; +} +.showShareButton.active:hover { + background-color: #afafaf; +} +.showShareButton.active:active { + background-color: white; +} +.showShareButton.inactive { + border-width: 0; + background-color: white; +} +.showShareButton.inactive:hover { + background-color: #afafaf; +} +.showShareButton.inactive:active { + background-color: white; +} +.showShareButtonDots { + display: flex; + width: 32px; + height: 40px; position: absolute; - top: 50%; - left: 7px; - transform: translate(0%, -50%); + top: 5px; + right: 14px; + pointer-events: none; } - -#snapshot-images img { - box-sizing: border-box; - padding: 0 7px 0 7px; -} - -#snapshot-images img.multiple { - padding-left: 28px; +.showShareButtonDots > span { + width: 10px; + height: 10px; + margin: auto; + background-color: #0093C5; + border-radius: 50%; + border-width: 0; + display: inline; } +/* +// END styling of share overlay +*/ +/* +// START styling of snapshot controls (bottom panel) and its contents +*/ #snapshot-controls { width: 100%; position: absolute; left: 0; - bottom: 14px; + overflow: hidden; + display: flex; + justify-content: center; +} +#snap-settings { + display: inline; + width: 150px; + margin: 2px auto 0 auto; +} +#snap-settings form input { + margin-bottom: 5px; } -.prompt { - font-family: Raleway-SemiBold; - font-size: 14px; -} - -div.button { - padding-top: 21px; -} - -.compound-button { - position: relative; - height: auto; -} - -.compound-button input { - padding-left: 40px; -} - -.compound-button .glyph { - display: inline-block; - position: absolute; - left: 12px; - top: 16px; - width: 23px; - height: 23px; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgaGVpZ2h0PSI0MCIKICAgd2lkdGg9IjQwIgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIKICAgdmlld0JveD0iMCAwIDQwIDQwIgogICB5PSIwcHgiCiAgIHg9IjBweCIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGEzNCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48ZGM6dGl0bGU+PC9kYzp0aXRsZT48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczMyIiAvPjxzdHlsZQogICAgIGlkPSJzdHlsZTQiCiAgICAgdHlwZT0idGV4dC9jc3MiPgoJLnN0MHtmaWxsOiM0MTQwNDI7fQoJLnN0MXtmaWxsOiNDQ0NDQ0M7fQoJLnN0MntmaWxsOiMxMzk4QkI7fQoJLnN0M3tmaWxsOiMzMUQ4RkY7fQo8L3N0eWxlPjxnCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTEwKSIKICAgICBpZD0iTGF5ZXJfMSI+PGNpcmNsZQogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJjaXJjbGUxMyIKICAgICAgIHI9IjQuNDQwMDAwMSIKICAgICAgIGN5PSIxMjYuMTciCiAgICAgICBjeD0iMjAuNTQwMDAxIgogICAgICAgY2xhc3M9InN0MSIgLz48cGF0aAogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMTUiCiAgICAgICBkPSJtIDI4Ljg3LDEzOS4yNiBjIDAuMDEsLTAuMDEgMC4wMiwtMC4wMiAwLjAzLC0wLjAzIGwgMCwtMS44NiBjIDAsLTIuNjggLTIuMzMsLTQuNzcgLTUsLTQuNzcgbCAtNi40MiwwIGMgLTIuNjgsMCAtNC44NSwyLjA5IC00Ljg1LDQuNzcgbCAwLDEuODggMTYuMjQsMCB6IgogICAgICAgY2xhc3M9InN0MSIgLz48cGF0aAogICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMTciCiAgICAgICBkPSJtIDM4LjE3LDEyMy40MiBjIDAsLTMuOTcgLTMuMjIsLTcuMTkgLTcuMTksLTcuMTkgbCAtMjAuMzEsMCBjIC0zLjk3LDAgLTcuMTksMy4yMiAtNy4xOSw3LjE5IGwgMCwxNC4xOCBjIDAsMy45NyAzLjIyLDcuMTkgNy4xOSw3LjE5IGwgMjAuMzEsMCBjIDMuOTcsMCA3LjE5LC0zLjIyIDcuMTksLTcuMTkgbCAwLC0xNC4xOCB6IG0gLTEuNzgsMTQuMjcgYyAwLDMuMDMgLTIuNDYsNS40OSAtNS40OSw1LjQ5IGwgLTIwLjMyLDAgYyAtMy4wMywwIC01LjQ5LC0yLjQ2IC01LjQ5LC01LjQ5IGwgMCwtMTQuMTkgYyAwLC0zLjAzIDIuNDYsLTUuNDkgNS40OSwtNS40OSBsIDIwLjMzLDAgYyAzLjAzLDAgNS40OSwyLjQ2IDUuNDksNS40OSBsIDAsMTQuMTkgeiIKICAgICAgIGNsYXNzPSJzdDEiIC8+PC9nPjxnCiAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwtMTEwKSIKICAgICBpZD0iTGF5ZXJfMiIgLz48L3N2Zz4=); - background-repeat: no-repeat; - background-size: 23px 23px; -} - -.setting { - display: inline-table; - height: 28px; -} - -.setting label { - display: table-cell; - vertical-align: middle; - font-family: Raleway-SemiBold; - font-size: 14px; -} - -.setting + .setting { - margin-left: 18px; -} - -input[type=button].naked { - font-size: 40px; - line-height: 40px; - width: 30px; +#snap-button { + width: 72px; + height: 72px; padding: 0; - margin: 0 0 -6px 0; - position: relative; - top: -6px; - left: -8px; - background: none; + border-radius: 50%; + background: #EA4C5F; + border: 3px solid white; + margin: 2px auto 0 auto; + box-sizing: content-box; + display: inline; + outline:none; } +#snap-button:disabled { + background: gray; +} +#snap-button:hover:enabled { + background: #C62147; +} +#snap-button:active:enabled { + background: #EA4C5F; +} +#snap-settings-right { + display: inline; + width: 150px; + margin: auto; +} +/* +// END styling of snapshot controls (bottom panel) and its contents +*/ -input[type=button].naked:hover { - color: #00b4ef; - background: none; +/* +// START misc styling +*/ +body { + padding: 0; + margin: 0; + overflow: hidden; } +p { + margin: 2px 0; +} +h4 { + margin: 14px 0 0 0; +} +.centeredImage { + margin: 0 auto; + display: block; +} +/* +// END misc styling +*/ diff --git a/scripts/system/html/css/hifi-style.css b/scripts/system/html/css/hifi-style.css new file mode 100644 index 0000000000..f1ace02eb0 --- /dev/null +++ b/scripts/system/html/css/hifi-style.css @@ -0,0 +1,170 @@ +/* +// hifi-style.css +// +// Created by Zach Fox on 2017-04-18 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +*/ + +@font-face { + font-family: Raleway-Regular; + src: url(../../../../resources/fonts/Raleway-Regular.ttf), /* Windows production */ + url(../../../../fonts/Raleway-Regular.ttf), /* OSX production */ + url(../../../../interface/resources/fonts/Raleway-Regular.ttf); /* Development, running script in /HiFi/examples */ +} + +@font-face { + font-family: Raleway-Light; + src: url(../../../../resources/fonts/Raleway-Light.ttf), + url(../../../../fonts/Raleway-Light.ttf), + url(../../../../interface/resources/fonts/Raleway-Light.ttf); +} + +@font-face { + font-family: Raleway-Bold; + src: url(../../../../resources/fonts/Raleway-Bold.ttf), + url(../../../../fonts/Raleway-Bold.ttf), + url(../../../../interface/resources/fonts/Raleway-Bold.ttf); +} + +@font-face { + font-family: Raleway-SemiBold; + src: url(../../../../resources/fonts/Raleway-SemiBold.ttf), + url(../../../../fonts/Raleway-SemiBold.ttf), + url(../../../../interface/resources/fonts/Raleway-SemiBold.ttf); +} + +@font-face { + font-family: FiraSans-SemiBold; + src: url(../../../../resources/fonts/FiraSans-SemiBold.ttf), + url(../../../../fonts/FiraSans-SemiBold.ttf), + url(../../../../interface/resources/fonts/FiraSans-SemiBold.ttf); +} + +@font-face { + font-family: AnonymousPro-Regular; + src: url(../../../../resources/fonts/AnonymousPro-Regular.ttf), + url(../../../../fonts/AnonymousPro-Regular.ttf), + url(../../../../interface/resources/fonts/AnonymousPro-Regular.ttf); +} + +@font-face { + font-family: HiFi-Glyphs; + src: url(../../../../resources/fonts/hifi-glyphs.ttf), + url(../../../../fonts/hifi-glyphs.ttf), + url(../../../../interface/resources/fonts/hifi-glyphs.ttf); +} + +body { + color: #afafaf; + background-color: #404040; + font-family: Raleway-Regular; + font-size: 15px; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + overflow-x: hidden; + overflow-y: auto; +} + +hr { + border: none; + background: #404040 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAjSURBVBhXY1RVVf3PgARYjIyMoEwIYHRwcEBRwQSloYCBAQCwjgPMiI7W2QAAAABJRU5ErkJggg==) repeat-x top left; + padding: 1px; + -webkit-margin-before: 0; + -webkit-margin-after: 0; + -webkit-margin-start: 0; + -webkit-margin-end: 0; + width: 100%; + position: absolute; +} + +.hifi-glyph { + font-family: HiFi-Glyphs; + border: none; + //margin: -10px; + padding: 0; +} + +input[type=radio] { + width: 2em; + margin: 0; + padding: 0; + font-size: 1em; + opacity: 0; +} +input[type=radio] + label{ + display: inline-block; + margin-left: -2em; + line-height: 2em; +} +input[type=radio] + label > span{ + display: inline-block; + width: 20px; + height: 20px; + margin: 5px; + border-radius: 50%; + background: #6B6A6B; + background-image: linear-gradient(#7D7D7D, #6B6A6B); + vertical-align: bottom; +} +input[type=radio]:checked + label > span{ + background-image: linear-gradient(#7D7D7D, #6B6A6B); +} +input[type=radio]:active + label > span, +input[type=radio]:hover + label > span{ + background-image: linear-gradient(#FFFFFF, #AFAFAF); +} +input[type=radio]:checked + label > span > span, +input[type=radio]:active + label > span > span{ + display: block; + width: 10px; + height: 10px; + margin: 3px; + border: 2px solid #36CDFF; + border-radius: 50%; + background: #00B4EF; +} + +.grayButton { + font-family: FiraSans-SemiBold; + color: white; + padding: 0px 10px; + border-width: 0px; + background-image: linear-gradient(#FFFFFF, #AFAFAF); +} +.grayButton:hover { + background-image: linear-gradient(#FFFFFF, #FFFFFF); +} +.grayButton:active { + background-image: linear-gradient(#AFAFAF, #AFAFAF); +} +.grayButton:disabled { + background-image: linear-gradient(#FFFFFF, ##AFAFAF); +} +.blueButton { + font-family: FiraSans-SemiBold; + color: white; + padding: 0px 10px; + border-radius: 3px; + border-width: 0px; + background-image: linear-gradient(#00B4EF, #1080B8); + min-height: 30px; + +} +.blueButton:hover { + background-image: linear-gradient(#00B4EF, #00B4EF); +} +.blueButton:active { + background-image: linear-gradient(#1080B8, #1080B8); +} +.blueButton:disabled { + background-image: linear-gradient(#FFFFFF, #AFAFAF); +} \ No newline at end of file diff --git a/scripts/system/html/img/fb_logo.png b/scripts/system/html/img/fb_logo.png new file mode 100644 index 0000000000..1de20bacd8 Binary files /dev/null and b/scripts/system/html/img/fb_logo.png differ diff --git a/scripts/system/html/img/shareIcon.png b/scripts/system/html/img/shareIcon.png new file mode 100644 index 0000000000..0486ac9202 Binary files /dev/null and b/scripts/system/html/img/shareIcon.png differ diff --git a/scripts/system/html/img/shareToFeed.png b/scripts/system/html/img/shareToFeed.png new file mode 100644 index 0000000000..f681c49d8f Binary files /dev/null and b/scripts/system/html/img/shareToFeed.png differ diff --git a/scripts/system/html/img/snapshotIcon.png b/scripts/system/html/img/snapshotIcon.png new file mode 100644 index 0000000000..5cb2742a32 Binary files /dev/null and b/scripts/system/html/img/snapshotIcon.png differ diff --git a/scripts/system/html/img/twitter_logo.png b/scripts/system/html/img/twitter_logo.png new file mode 100644 index 0000000000..59fd027c2a Binary files /dev/null and b/scripts/system/html/img/twitter_logo.png differ diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index f140c54e09..53f4d17930 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -10,117 +10,325 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var paths = [], idCounter = 0, imageCount; -function addImage(data) { - if (!data.localPath) { +var paths = []; +var idCounter = 0; +var imageCount = 0; +function showSetupInstructions() { + var snapshotImagesDiv = document.getElementById("snapshot-images"); + snapshotImagesDiv.className = "snapshotInstructions"; + snapshotImagesDiv.innerHTML = '<img class="centeredImage" src="./img/snapshotIcon.png" alt="Snapshot Instructions" width="64" height="64"/>' + + '<br/>' + + '<p>This app lets you take and share snaps and GIFs with your connections in High Fidelity.</p>' + + "<h4>Setup Instructions</h4>" + + "<p>Before you can begin taking snaps, please choose where you'd like to save snaps on your computer:</p>" + + '<br/>' + + '<div style="text-align:center;">' + + '<input class="blueButton" style="margin-left:auto;margin-right:auto;width:130px;" type="button" value="CHOOSE" onclick="chooseSnapshotLocation()" />' + + '</div>'; + document.getElementById("snap-button").disabled = true; +} +function showSetupComplete() { + var snapshotImagesDiv = document.getElementById("snapshot-images"); + snapshotImagesDiv.className = "snapshotInstructions"; + snapshotImagesDiv.innerHTML = '<img class="centeredImage" src="./img/snapshotIcon.png" alt="Snapshot Instructions" width="64" height="64"/>' + + '<br/>' + + "<h4>You're all set!</h4>" + + '<p>Try taking a snapshot by pressing the red button below.</p>'; +} +function chooseSnapshotLocation() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "chooseSnapshotLocation" + })); +} +function clearImages() { + document.getElementById("snap-button").disabled = false; + var snapshotImagesDiv = document.getElementById("snapshot-images"); + snapshotImagesDiv.classList.remove("snapshotInstructions"); + while (snapshotImagesDiv.hasChildNodes()) { + snapshotImagesDiv.removeChild(snapshotImagesDiv.lastChild); + } + paths = []; + imageCount = 0; + idCounter = 0; +} +function addImage(image_data, isGifLoading, isShowingPreviousImages, canSharePreviousImages, hifiShareButtonsDisabled) { + if (!image_data.localPath) { return; } - var div = document.createElement("DIV"), - input = document.createElement("INPUT"), - label = document.createElement("LABEL"), - img = document.createElement("IMG"), - div2 = document.createElement("DIV"), - id = "p" + idCounter++; - img.id = id + "img"; - function toggle() { data.share = input.checked; } - div.style.height = "" + Math.floor(100 / imageCount) + "%"; + var id = "p" + idCounter++; + // imageContainer setup + var imageContainer = document.createElement("DIV"); + imageContainer.id = id; + imageContainer.style.width = "100%"; + imageContainer.style.height = "251px"; + imageContainer.style.display = "flex"; + imageContainer.style.justifyContent = "center"; + imageContainer.style.alignItems = "center"; + imageContainer.style.position = "relative"; + // img setup + var img = document.createElement("IMG"); + img.id = id + "img"; if (imageCount > 1) { img.setAttribute("class", "multiple"); } - img.src = data.localPath; - div.appendChild(img); - if (imageCount > 1) { // I'd rather use css, but the included stylesheet is quite particular. - // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. - label.setAttribute('for', id); // cannot do label.for = - input.id = id; - input.type = "checkbox"; - input.checked = false; - data.share = input.checked; - input.addEventListener('change', toggle); - div2.setAttribute("class", "property checkbox"); - div2.appendChild(input); - div2.appendChild(label); - div.appendChild(div2); - } else { - data.share = true; + img.src = image_data.localPath; + imageContainer.appendChild(img); + document.getElementById("snapshot-images").appendChild(imageContainer); + paths.push(image_data.localPath); + var isGif = img.src.split('.').pop().toLowerCase() === "gif"; + if (isGif) { + imageContainer.innerHTML += '<span class="gifLabel">GIF</span>'; + } + if (!isGifLoading && !isShowingPreviousImages) { + shareForUrl(id); + } else if (isShowingPreviousImages && canSharePreviousImages) { + appendShareBar(id, image_data.story_id, isGif, hifiShareButtonsDisabled) } - document.getElementById("snapshot-images").appendChild(div); - paths.push(data); } -function handleShareButtons(messageOptions) { - var openFeed = document.getElementById('openFeed'); - openFeed.checked = messageOptions.openFeedAfterShare; - openFeed.onchange = function () { +function appendShareBar(divID, story_id, isGif, hifiShareButtonsDisabled) { + var story_url = "https://highfidelity.com/user_stories/" + story_id; + var parentDiv = document.getElementById(divID); + parentDiv.setAttribute('data-story-id', story_id); + document.getElementById(divID).appendChild(createShareBar(divID, isGif, story_url, hifiShareButtonsDisabled)); + if (divID === "p0") { + selectImageToShare(divID, true); + } +} +function createShareBar(parentID, isGif, shareURL, hifiShareButtonsDisabled) { + var shareBar = document.createElement("div"); + shareBar.id = parentID + "shareBar"; + shareBar.className = "shareControls"; + var shareButtonsDivID = parentID + "shareButtonsDiv"; + var showShareButtonsButtonDivID = parentID + "showShareButtonsButtonDiv"; + var showShareButtonsButtonID = parentID + "showShareButtonsButton"; + var showShareButtonsLabelID = parentID + "showShareButtonsLabel"; + var blastToConnectionsButtonID = parentID + "blastToConnectionsButton"; + var shareWithEveryoneButtonID = parentID + "shareWithEveryoneButton"; + var facebookButtonID = parentID + "facebookButton"; + var twitterButtonID = parentID + "twitterButton"; + shareBar.innerHTML += '' + + '<div class="shareButtons" id="' + shareButtonsDivID + '" style="visibility:hidden">' + + '<input type="button"' + (hifiShareButtonsDisabled ? ' disabled' : '') + ' class="blastToConnections blueButton" id="' + blastToConnectionsButtonID + '" value="BLAST TO MY CONNECTIONS" onclick="blastToConnections(' + parentID + ', ' + isGif + ')" />' + + '<input type="button"' + (hifiShareButtonsDisabled ? ' disabled' : '') + ' class="shareWithEveryone" id="' + shareWithEveryoneButtonID + '" onclick="shareWithEveryone(' + parentID + ', ' + isGif + ')" />' + + '<a class="facebookButton" id="' + facebookButtonID + '" onclick="shareButtonClicked(' + parentID + ')" target="_blank" href="https://www.facebook.com/dialog/feed?app_id=1585088821786423&link=' + shareURL + '"></a>' + + '<a class="twitterButton" id="' + twitterButtonID + '" onclick="shareButtonClicked(' + parentID + ')" target="_blank" href="https://twitter.com/intent/tweet?text=I%20just%20took%20a%20snapshot!&url=' + shareURL + '&via=highfidelity&hashtags=VR,HiFi"></a>' + + '</div>' + + '<div class="showShareButtonsButtonDiv" id="' + showShareButtonsButtonDivID + '">' + + '<label id="' + showShareButtonsLabelID + '" for="' + showShareButtonsButtonID + '">SHARE</label>' + + '<input type="button" class="showShareButton inactive" id="' + showShareButtonsButtonID + '" onclick="selectImageToShare(' + parentID + ', true)" />' + + '<div class="showShareButtonDots">' + + '<span></span><span></span><span></span>' + + '</div>' + + '</div>'; + + // Add onclick handler to parent DIV's img to toggle share buttons + document.getElementById(parentID + 'img').onclick = function () { selectImageToShare(parentID, true) }; + + return shareBar; +} +function selectImageToShare(selectedID, isSelected) { + if (selectedID.id) { + selectedID = selectedID.id; // sometimes (?), `selectedID` is passed as an HTML object to these functions; we just want the ID + } + var imageContainer = document.getElementById(selectedID); + var image = document.getElementById(selectedID + 'img'); + var shareBar = document.getElementById(selectedID + "shareBar"); + var shareButtonsDiv = document.getElementById(selectedID + "shareButtonsDiv"); + var showShareButtonsButton = document.getElementById(selectedID + "showShareButtonsButton"); + + if (isSelected) { + showShareButtonsButton.onclick = function () { selectImageToShare(selectedID, false) }; + showShareButtonsButton.classList.remove("inactive"); + showShareButtonsButton.classList.add("active"); + + image.onclick = function () { selectImageToShare(selectedID, false) }; + imageContainer.style.outline = "4px solid #00b4ef"; + imageContainer.style.outlineOffset = "-4px"; + + shareBar.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; + + shareButtonsDiv.style.visibility = "visible"; + } else { + showShareButtonsButton.onclick = function () { selectImageToShare(selectedID, true) }; + showShareButtonsButton.classList.remove("active"); + showShareButtonsButton.classList.add("inactive"); + + image.onclick = function () { selectImageToShare(selectedID, true) }; + imageContainer.style.outline = "none"; + + shareBar.style.backgroundColor = "rgba(0, 0, 0, 0.0)"; + + shareButtonsDiv.style.visibility = "hidden"; + } +} +function shareForUrl(selectedID) { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "shareSnapshotForUrl", + data: paths[parseInt(selectedID.substring(1))] + })); +} +function blastToConnections(selectedID, isGif) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + + document.getElementById(selectedID + "blastToConnectionsButton").disabled = true; + document.getElementById(selectedID + "shareWithEveryoneButton").disabled = true; + + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "blastToConnections", + story_id: document.getElementById(selectedID).getAttribute("data-story-id"), + isGif: isGif + })); +} +function shareWithEveryone(selectedID, isGif) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + + document.getElementById(selectedID + "blastToConnectionsButton").disabled = true; + document.getElementById(selectedID + "shareWithEveryoneButton").disabled = true; + + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "shareSnapshotWithEveryone", + story_id: document.getElementById(selectedID).getAttribute("data-story-id"), + isGif: isGif + })); +} +function shareButtonClicked(selectedID) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "shareButtonClicked", + story_id: document.getElementById(selectedID).getAttribute("data-story-id") + })); +} +function cancelSharing(selectedID) { + selectedID = selectedID.id; // `selectedID` is passed as an HTML object to these functions; we just want the ID + var shareBar = document.getElementById(selectedID + "shareBar"); + + shareBar.style.display = "inline"; +} + +function handleCaptureSetting(setting) { + var stillAndGif = document.getElementById('stillAndGif'); + var stillOnly = document.getElementById('stillOnly'); + stillAndGif.checked = setting; + stillOnly.checked = !setting; + + stillAndGif.onclick = function () { EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", - action: (openFeed.checked ? "setOpenFeedTrue" : "setOpenFeedFalse") + action: "captureStillAndGif" })); - }; - - if (!messageOptions.canShare) { - // this means you may or may not be logged in, but can't share - // because you are not in a public place. - document.getElementById("sharing").innerHTML = "<p class='prompt'>Snapshots can be shared when they're taken in shareable places."; } + stillOnly.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "captureStillOnly" + })); + } + } window.onload = function () { - // Something like the following will allow testing in a browser. - //addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'}); - //addImage({ localPath: 'http://lorempixel.com/1512/1680' }); + // Uncomment the line below to test functionality in a browser. + // See definition of "testInBrowser()" to modify tests. + //testInBrowser(true); openEventBridge(function () { // Set up a handler for receiving the data, and tell the .js we are ready to receive it. EventBridge.scriptEventReceived.connect(function (message) { + message = JSON.parse(message); + if (message.type !== "snapshot") { return; } + + switch (message.action) { + case 'showSetupInstructions': + showSetupInstructions(); + break; + case 'snapshotLocationChosen': + clearImages(); + showSetupComplete(); + break; + case 'clearPreviousImages': + clearImages(); + break; + case 'showPreviousImages': + clearImages(); + var messageOptions = message.options; + imageCount = message.image_data.length; + message.image_data.forEach(function (element, idx, array) { + addImage(element, true, true, message.canShare, message.image_data[idx].buttonDisabled); + }); + break; + case 'addImages': + // The last element of the message contents list contains a bunch of options, + // including whether or not we can share stuff + // The other elements of the list contain image paths. + var messageOptions = message.options; - // The last element of the message contents list contains a bunch of options, - // including whether or not we can share stuff - // The other elements of the list contain image paths. - var messageOptions = message.action.pop(); - handleShareButtons(messageOptions); + if (messageOptions.containsGif) { + if (messageOptions.processingGif) { + imageCount = message.image_data.length + 1; // "+1" for the GIF that'll finish processing soon + message.image_data.unshift({ localPath: messageOptions.loadingGifPath }); + message.image_data.forEach(function (element, idx, array) { + addImage(element, idx === 0, false, false); + }); + } else { + var gifPath = message.image_data[0].localPath; + var p0img = document.getElementById('p0img'); + p0img.src = gifPath; - if (messageOptions.containsGif) { - if (messageOptions.processingGif) { - imageCount = message.action.length + 1; // "+1" for the GIF that'll finish processing soon - message.action.unshift({ localPath: messageOptions.loadingGifPath }); - message.action.forEach(addImage); - document.getElementById('p0').disabled = true; - } else { - var gifPath = message.action[0].localPath; - document.getElementById('p0').disabled = false; - document.getElementById('p0img').src = gifPath; - paths[0].localPath = gifPath; - } - } else { - imageCount = message.action.length; - message.action.forEach(addImage); + paths[0] = gifPath; + shareForUrl("p0"); + } + } else { + imageCount = message.image_data.length; + message.image_data.forEach(function (element, idx, array) { + addImage(element, false, false, false); + }); + } + break; + case 'captureSettings': + handleCaptureSetting(message.setting); + break; + case 'snapshotUploadComplete': + var isGif = message.image_url.split('.').pop().toLowerCase() === "gif"; + appendShareBar(isGif || imageCount === 1 ? "p0" : "p1", message.story_id, isGif); + break; + default: + console.log("Unknown message action received in SnapshotReview.js."); + break; } }); + EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", action: "ready" })); - }); - + });; }; -// beware of bug: Cannot send objects at top level. (Nested in arrays is fine.) -function shareSelected() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "snapshot", - action: paths - })); -} -function doNotShare() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "snapshot", - action: [] - })); -} function snapshotSettings() { EventBridge.emitWebEvent(JSON.stringify({ type: "snapshot", action: "openSettings" })); } +function takeSnapshot() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "takeSnapshot" + })); +} + +function testInBrowser(isTestingSetupInstructions) { + if (isTestingSetupInstructions) { + showSetupInstructions(); + } else { + imageCount = 1; + //addImage({ localPath: 'http://lorempixel.com/553/255' }); + addImage({ localPath: 'C:/Users/valef/Desktop/hifi-snap-by-zfox-on-2017-04-26_10-26-53.gif' }, false, true, true, false); + } +} diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 6039bc09c1..1cc24b8265 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -7,7 +7,7 @@ // Distributed under the Apache License, Version 2.0 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar */ +/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar, Snapshot */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -24,28 +24,79 @@ var buttonConnected = false; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ icon: "icons/tablet-icons/snap-i.svg", + activeIcon: "icons/tablet-icons/snap-a.svg", text: buttonName, sortOrder: 5 }); -function shouldOpenFeedAfterShare() { - var persisted = Settings.getValue('openFeedAfterShare', true); // might answer true, false, "true", or "false" - return persisted && (persisted !== 'false'); +var snapshotOptions; +var imageData = []; +var storyIDsToMaybeDelete = []; +var shareAfterLogin = false; +var snapshotToShareAfterLogin; +var METAVERSE_BASE = location.metaverseServerUrl; + +// It's totally unnecessary to return to C++ to perform many of these requests, such as DELETEing an old story, +// POSTING a new one, PUTTING a new audience, or GETTING story data. It's far more efficient to do all of that within JS +function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. + var httpRequest = new XMLHttpRequest(), key; + // QT bug: apparently doesn't handle onload. Workaround using readyState. + httpRequest.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (httpRequest.readyState >= READY_STATE_DONE) { + var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, + response = !error && httpRequest.responseText, + contentType = !error && httpRequest.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + callback(error, response); + } + }; + if (typeof options === 'string') { + options = { uri: options }; + } + if (options.url) { + options.uri = options.url; + } + if (!options.method) { + options.method = 'GET'; + } + if (options.body && (options.method === 'GET')) { // add query parameters + var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; + for (key in options.body) { + params.push(key + '=' + options.body[key]); + } + options.uri += appender + params.join('&'); + delete options.body; + } + if (options.json) { + options.headers = options.headers || {}; + options.headers["Content-type"] = "application/json"; + options.body = JSON.stringify(options.body); + } + for (key in options.headers || {}) { + httpRequest.setRequestHeader(key, options.headers[key]); + } + httpRequest.open(options.method, options.uri, true); + httpRequest.send(options.body); } -function showFeedWindow() { - if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) - || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) { - tablet.loadQMLSource("TabletAddressDialog.qml"); + +function openLoginWindow() { + if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { + Menu.triggerOption("Login / Sign Up"); } else { - tablet.initialScreen("TabletAddressDialog.qml"); + tablet.loadQMLOnTop("../../dialogs/TabletLoginDialog.qml"); HMD.openTablet(); } } -var outstanding; -var readyData; -var shareAfterLogin = false; -var snapshotToShareAfterLogin; function onMessage(message) { // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) @@ -58,91 +109,257 @@ function onMessage(message) { } var isLoggedIn; - var needsLogin = false; switch (message.action) { - case 'ready': // Send it. + case 'ready': // DOM is ready and page has loaded tablet.emitScriptEvent(JSON.stringify({ type: "snapshot", - action: readyData + action: "captureSettings", + setting: Settings.getValue("alsoTakeAnimatedSnapshot", true) })); - outstanding = 0; + if (Snapshot.getSnapshotsLocation() !== "") { + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "showPreviousImages", + options: snapshotOptions, + image_data: imageData, + canShare: !isDomainOpen(Settings.getValue("previousSnapshotDomainID")) + })); + } else { + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "showSetupInstructions" + })); + Settings.setValue("previousStillSnapPath", ""); + Settings.setValue("previousStillSnapStoryID", ""); + Settings.setValue("previousStillSnapSharingDisabled", false); + Settings.setValue("previousAnimatedSnapPath", ""); + Settings.setValue("previousAnimatedSnapStoryID", ""); + Settings.setValue("previousAnimatedSnapSharingDisabled", false); + } + break; + case 'chooseSnapshotLocation': + var snapshotPath = Window.browseDir("Choose Snapshots Directory", "", ""); + + if (snapshotPath) { // not cancelled + Snapshot.setSnapshotsLocation(snapshotPath); + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "snapshotLocationChosen" + })); + } break; case 'openSettings': - if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) - || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) { + if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "General Preferences"); } else { tablet.loadQMLOnTop("TabletGeneralPreferences.qml"); } break; - case 'setOpenFeedFalse': - Settings.setValue('openFeedAfterShare', false); + case 'captureStillAndGif': + print("Changing Snapshot Capture Settings to Capture Still + GIF"); + Settings.setValue("alsoTakeAnimatedSnapshot", true); break; - case 'setOpenFeedTrue': - Settings.setValue('openFeedAfterShare', true); + case 'captureStillOnly': + print("Changing Snapshot Capture Settings to Capture Still Only"); + Settings.setValue("alsoTakeAnimatedSnapshot", false); + break; + case 'takeSnapshot': + takeSnapshot(); + break; + case 'shareSnapshotForUrl': + isLoggedIn = Account.isLoggedIn(); + if (isLoggedIn) { + print('Sharing snapshot with audience "for_url":', message.data); + Window.shareSnapshot(message.data, message.href || href); + } else { + // TODO + } + break; + case 'blastToConnections': + isLoggedIn = Account.isLoggedIn(); + storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1); + if (message.isGif) { + Settings.setValue("previousAnimatedSnapSharingDisabled", true); + } else { + Settings.setValue("previousStillSnapSharingDisabled", true); + } + + if (isLoggedIn) { + print('Uploading new story for announcement!'); + + request({ + uri: METAVERSE_BASE + '/api/v1/user_stories/' + message.story_id, + method: 'GET' + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("ERROR getting details about existing snapshot story:", error || response.status); + return; + } else { + var requestBody = { + user_story: { + audience: "for_connections", + action: "announcement", + path: response.user_story.path, + place_name: response.user_story.place_name, + thumbnail_url: response.user_story.thumbnail_url, + // For historical reasons, the server doesn't take nested JSON objects. + // Thus, I'm required to STRINGIFY what should be a nested object. + details: JSON.stringify({ + shareable_url: response.user_story.details.shareable_url, + image_url: response.user_story.details.image_url + }) + } + } + request({ + uri: METAVERSE_BASE + '/api/v1/user_stories', + method: 'POST', + json: true, + body: requestBody + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("ERROR uploading announcement story: ", error || response.status); + if (message.isGif) { + Settings.setValue("previousAnimatedSnapSharingDisabled", false); + } else { + Settings.setValue("previousStillSnapSharingDisabled", false); + } + return; + } else { + print("SUCCESS uploading announcement story! Story ID:", response.user_story.id); + } + }); + } + }); + + } else { + openLoginWindow(); + } + break; + case 'shareSnapshotWithEveryone': + isLoggedIn = Account.isLoggedIn(); + storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1); + if (message.isGif) { + Settings.setValue("previousAnimatedSnapSharingDisabled", true); + } else { + Settings.setValue("previousStillSnapSharingDisabled", true); + } + + if (isLoggedIn) { + print('Modifying audience of story ID', message.story_id, "to 'for_feed'"); + var requestBody = { + audience: "for_feed" + } + + if (message.isAnnouncement) { + requestBody.action = "announcement"; + print('...Also announcing!'); + } + request({ + uri: METAVERSE_BASE + '/api/v1/user_stories/' + message.story_id, + method: 'PUT', + json: true, + body: requestBody + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("ERROR changing audience: ", error || response.status); + if (message.isGif) { + Settings.setValue("previousAnimatedSnapSharingDisabled", false); + } else { + Settings.setValue("previousStillSnapSharingDisabled", false); + } + return; + } else { + print("SUCCESS changing audience" + (message.isAnnouncement ? " and posting announcement!" : "!")); + } + }); + } else { + openLoginWindow(); + shareAfterLogin = true; + snapshotToShareAfterLogin = { path: message.data, href: message.href || href }; + } + break; + case 'shareButtonClicked': + print('Twitter or FB "Share" button clicked! Removing ID', message.story_id, 'from storyIDsToMaybeDelete[].'); + storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(message.story_id), 1); + print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete)); break; default: - //tablet.webEventReceived.disconnect(onMessage); // <<< It's probably this that's missing?! - HMD.closeTablet(); - isLoggedIn = Account.isLoggedIn(); - message.action.forEach(function (submessage) { - if (submessage.share && !isLoggedIn) { - needsLogin = true; - submessage.share = false; - shareAfterLogin = true; - snapshotToShareAfterLogin = {path: submessage.localPath, href: submessage.href || href}; - } - if (submessage.share) { - print('sharing', submessage.localPath); - outstanding = true; - Window.shareSnapshot(submessage.localPath, submessage.href || href); - } else { - print('not sharing', submessage.localPath); - } - - }); - if (outstanding && shouldOpenFeedAfterShare()) { - showFeedWindow(); - outstanding = false; - } - if (needsLogin) { // after the possible feed, so that the login is on top - var isLoggedIn = Account.isLoggedIn(); - - if (!isLoggedIn) { - if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) - || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) { - Menu.triggerOption("Login / Sign Up"); - } else { - tablet.loadQMLOnTop("../../dialogs/TabletLoginDialog.qml"); - HMD.openTablet(); - } - } - } + print('Unknown message action received by snapshot.js!'); + break; } } var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); var isInSnapshotReview = false; -function confirmShare(data) { - tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL); - readyData = data; - tablet.webEventReceived.connect(onMessage); - HMD.openTablet(); - isInSnapshotReview = true; +var shouldActivateButton = false; +function onButtonClicked() { + if (isInSnapshotReview){ + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + shouldActivateButton = true; + var previousStillSnapPath = Settings.getValue("previousStillSnapPath"); + var previousStillSnapStoryID = Settings.getValue("previousStillSnapStoryID"); + var previousStillSnapSharingDisabled = Settings.getValue("previousStillSnapSharingDisabled"); + var previousAnimatedSnapPath = Settings.getValue("previousAnimatedSnapPath"); + var previousAnimatedSnapStoryID = Settings.getValue("previousAnimatedSnapStoryID"); + var previousAnimatedSnapSharingDisabled = Settings.getValue("previousAnimatedSnapSharingDisabled"); + snapshotOptions = { + containsGif: previousAnimatedSnapPath !== "", + processingGif: false, + shouldUpload: false + } + imageData = []; + if (previousAnimatedSnapPath !== "") { + imageData.push({ localPath: previousAnimatedSnapPath, story_id: previousAnimatedSnapStoryID, buttonDisabled: previousAnimatedSnapSharingDisabled }); + } + if (previousStillSnapPath !== "") { + imageData.push({ localPath: previousStillSnapPath, story_id: previousStillSnapStoryID, buttonDisabled: previousStillSnapSharingDisabled }); + } + tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL); + tablet.webEventReceived.connect(onMessage); + HMD.openTablet(); + isInSnapshotReview = true; + } } -function snapshotShared(errorMessage) { - if (!errorMessage) { - print('snapshot uploaded and shared'); +function snapshotUploaded(isError, reply) { + if (!isError) { + var replyJson = JSON.parse(reply); + var storyID = replyJson.user_story.id; + storyIDsToMaybeDelete.push(storyID); + var imageURL = replyJson.user_story.details.image_url; + var isGif = imageURL.split('.').pop().toLowerCase() === "gif"; + print('SUCCESS: Snapshot uploaded! Story with audience:for_url created! ID:', storyID); + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "snapshotUploadComplete", + story_id: storyID, + image_url: imageURL, + })); + if (isGif) { + Settings.setValue("previousAnimatedSnapStoryID", storyID); + } else { + Settings.setValue("previousStillSnapStoryID", storyID); + } } else { - print(errorMessage); - } - if ((--outstanding <= 0) && shouldOpenFeedAfterShare()) { - showFeedWindow(); + print(reply); } } var href, domainId; -function onClicked() { +function takeSnapshot() { + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "clearPreviousImages" + })); + Settings.setValue("previousStillSnapPath", ""); + Settings.setValue("previousStillSnapStoryID", ""); + Settings.setValue("previousStillSnapSharingDisabled", false); + Settings.setValue("previousAnimatedSnapPath", ""); + Settings.setValue("previousAnimatedSnapStoryID", ""); + Settings.setValue("previousAnimatedSnapSharingDisabled", false); + // Raising the desktop for the share dialog at end will interact badly with clearOverlayWhenMoving. // Turn it off now, before we start futzing with things (and possibly moving). clearOverlayWhenMoving = MyAvatar.getClearOverlayWhenMoving(); // Do not use Settings. MyAvatar keeps a separate copy. @@ -152,14 +369,25 @@ function onClicked() { // Even the domainId could change (e.g., if the user falls into a teleporter while recording). href = location.href; domainId = location.domainId; + Settings.setValue("previousSnapshotDomainID", domainId); + + maybeDeleteSnapshotStories(); // update button states - resetOverlays = Menu.isOptionChecked("Overlays"); // For completness. Certainly true if the button is visible to be clicke. + resetOverlays = Menu.isOptionChecked("Overlays"); // For completeness. Certainly true if the button is visible to be clicked. reticleVisible = Reticle.visible; Reticle.visible = false; - Window.stillSnapshotTaken.connect(stillSnapshotTaken); - Window.processingGifStarted.connect(processingGifStarted); - Window.processingGifCompleted.connect(processingGifCompleted); + + var includeAnimated = Settings.getValue("alsoTakeAnimatedSnapshot", true); + if (includeAnimated) { + Window.processingGifStarted.connect(processingGifStarted); + } else { + Window.stillSnapshotTaken.connect(stillSnapshotTaken); + } + if (buttonConnected) { + button.clicked.disconnect(onButtonClicked); + buttonConnected = false; + } // hide overlays if they are on if (resetOverlays) { @@ -170,13 +398,17 @@ function onClicked() { Script.setTimeout(function () { HMD.closeTablet(); Script.setTimeout(function () { - Window.takeSnapshot(false, true, 1.91); + Window.takeSnapshot(false, includeAnimated, 1.91); }, SNAPSHOT_DELAY); }, FINISH_SOUND_DELAY); } function isDomainOpen(id) { - var request = new XMLHttpRequest(); + print("Checking open status of domain with ID:", id); + if (!id) { + return false; + } + var options = [ 'now=' + new Date().toISOString(), 'include_actions=concurrency', @@ -184,15 +416,19 @@ function isDomainOpen(id) { 'restriction=open,hifi' // If we're sharing, we're logged in // If we're here, protocol matches, and it is online ]; - var url = location.metaverseServerUrl + "/api/v1/user_stories?" + options.join('&'); - request.open("GET", url, false); - request.send(); - if (request.status !== 200) { - return false; - } - var response = JSON.parse(request.response); // Not parsed for us. - return (response.status === 'success') && - response.total_entries; + var url = METAVERSE_BASE + "/api/v1/user_stories?" + options.join('&'); + + return request({ + uri: url, + method: 'GET' + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("ERROR getting open status of domain: ", error || response.status); + return false; + } else { + return response.total_entries; + } + }); } function stillSnapshotTaken(pathStillSnapshot, notify) { @@ -203,20 +439,30 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { Menu.setIsOptionChecked("Overlays", true); } Window.stillSnapshotTaken.disconnect(stillSnapshotTaken); + if (!buttonConnected) { + button.clicked.connect(onButtonClicked); + buttonConnected = true; + } // A Snapshot Review dialog might be left open indefinitely after taking the picture, // during which time the user may have moved. So stash that info in the dialog so that // it records the correct href. (We can also stash in .jpegs, but not .gifs.) // last element in data array tells dialog whether we can share or not - var confirmShareContents = [ - { localPath: pathStillSnapshot, href: href }, - { - containsGif: false, - processingGif: false, - canShare: !!isDomainOpen(domainId), - openFeedAfterShare: shouldOpenFeedAfterShare() - }]; - confirmShare(confirmShareContents); + snapshotOptions = { + containsGif: false, + processingGif: false, + canShare: !isDomainOpen(domainId) + }; + imageData = [{ localPath: pathStillSnapshot, href: href }]; + Settings.setValue("previousStillSnapPath", pathStillSnapshot); + + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "addImages", + options: snapshotOptions, + image_data: imageData + })); + if (clearOverlayWhenMoving) { MyAvatar.setClearOverlayWhenMoving(true); // not until after the share dialog } @@ -225,8 +471,7 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { function processingGifStarted(pathStillSnapshot) { Window.processingGifStarted.disconnect(processingGifStarted); - button.clicked.disconnect(onClicked); - buttonConnected = false; + Window.processingGifCompleted.connect(processingGifCompleted); // show hud Reticle.visible = reticleVisible; // show overlays if they were on @@ -234,16 +479,22 @@ function processingGifStarted(pathStillSnapshot) { Menu.setIsOptionChecked("Overlays", true); } - var confirmShareContents = [ - { localPath: pathStillSnapshot, href: href }, - { - containsGif: true, - processingGif: true, - loadingGifPath: Script.resolvePath(Script.resourcesPath() + 'icons/loadingDark.gif'), - canShare: !!isDomainOpen(domainId), - openFeedAfterShare: shouldOpenFeedAfterShare() - }]; - confirmShare(confirmShareContents); + snapshotOptions = { + containsGif: true, + processingGif: true, + loadingGifPath: Script.resolvePath(Script.resourcesPath() + 'icons/loadingDark.gif'), + canShare: !isDomainOpen(domainId) + }; + imageData = [{ localPath: pathStillSnapshot, href: href }]; + Settings.setValue("previousStillSnapPath", pathStillSnapshot); + + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: "addImages", + options: snapshotOptions, + image_data: imageData + })); + if (clearOverlayWhenMoving) { MyAvatar.setClearOverlayWhenMoving(true); // not until after the share dialog } @@ -252,57 +503,72 @@ function processingGifStarted(pathStillSnapshot) { function processingGifCompleted(pathAnimatedSnapshot) { Window.processingGifCompleted.disconnect(processingGifCompleted); - button.clicked.connect(onClicked); - buttonConnected = true; + if (!buttonConnected) { + button.clicked.connect(onButtonClicked); + buttonConnected = true; + } - var confirmShareContents = [ - { localPath: pathAnimatedSnapshot, href: href }, - { - containsGif: true, - processingGif: false, - canShare: !!isDomainOpen(domainId), - openFeedAfterShare: shouldOpenFeedAfterShare() - }]; - readyData = confirmShareContents; + snapshotOptions = { + containsGif: true, + processingGif: false, + canShare: !isDomainOpen(domainId) + } + imageData = [{ localPath: pathAnimatedSnapshot, href: href }]; + Settings.setValue("previousAnimatedSnapPath", pathAnimatedSnapshot); tablet.emitScriptEvent(JSON.stringify({ type: "snapshot", - action: readyData + action: "addImages", + options: snapshotOptions, + image_data: imageData })); } - +function maybeDeleteSnapshotStories() { + storyIDsToMaybeDelete.forEach(function (element, idx, array) { + request({ + uri: METAVERSE_BASE + '/api/v1/user_stories/' + element, + method: 'DELETE' + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("ERROR deleting snapshot story: ", error || response.status); + return; + } else { + print("SUCCESS deleting snapshot story with ID", element); + } + }) + }); + storyIDsToMaybeDelete = []; +} function onTabletScreenChanged(type, url) { + button.editProperties({ isActive: shouldActivateButton }); + shouldActivateButton = false; if (isInSnapshotReview) { tablet.webEventReceived.disconnect(onMessage); isInSnapshotReview = false; } } -function onConnected() { +function onUsernameChanged() { if (shareAfterLogin && Account.isLoggedIn()) { - print('sharing', snapshotToShareAfterLogin.path); + print('Sharing snapshot after login:', snapshotToShareAfterLogin.path); Window.shareSnapshot(snapshotToShareAfterLogin.path, snapshotToShareAfterLogin.href); shareAfterLogin = false; - if (shouldOpenFeedAfterShare()) { - showFeedWindow(); - } } - } -button.clicked.connect(onClicked); +button.clicked.connect(onButtonClicked); buttonConnected = true; -Window.snapshotShared.connect(snapshotShared); +Window.snapshotShared.connect(snapshotUploaded); tablet.screenChanged.connect(onTabletScreenChanged); -Account.usernameChanged.connect(onConnected); +Account.usernameChanged.connect(onUsernameChanged); Script.scriptEnding.connect(function () { if (buttonConnected) { - button.clicked.disconnect(onClicked); + button.clicked.disconnect(onButtonClicked); buttonConnected = false; } if (tablet) { tablet.removeButton(button); } - Window.snapshotShared.disconnect(snapshotShared); + Window.snapshotShared.disconnect(snapshotUploaded); tablet.screenChanged.disconnect(onTabletScreenChanged); });