Merge remote-tracking branch 'upstream/master' into android_goto_splash

This commit is contained in:
Gabriel Calero 2018-03-23 16:56:53 -03:00
commit 8558759d41
77 changed files with 2001 additions and 596 deletions

View file

@ -46,15 +46,68 @@ static const uint8_t CPU_AFFINITY_COUNT_LOW = 1;
static const int INTERFACE_RUNNING_CHECK_FREQUENCY_MS = 1000;
#endif
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
static const QStringList BAKEABLE_MODEL_EXTENSIONS = { "fbx" };
static QStringList BAKEABLE_TEXTURE_EXTENSIONS;
static const QStringList BAKEABLE_SCRIPT_EXTENSIONS = {};
static const QStringList BAKEABLE_SCRIPT_EXTENSIONS = { };
static const QString BAKED_MODEL_SIMPLE_NAME = "asset.fbx";
static const QString BAKED_TEXTURE_SIMPLE_NAME = "texture.ktx";
static const QString BAKED_SCRIPT_SIMPLE_NAME = "asset.js";
static const ModelBakeVersion CURRENT_MODEL_BAKE_VERSION = (ModelBakeVersion)((BakeVersion)ModelBakeVersion::COUNT - 1);
static const TextureBakeVersion CURRENT_TEXTURE_BAKE_VERSION = (TextureBakeVersion)((BakeVersion)TextureBakeVersion::COUNT - 1);
static const ScriptBakeVersion CURRENT_SCRIPT_BAKE_VERSION = (ScriptBakeVersion)((BakeVersion)ScriptBakeVersion::COUNT - 1);
BakedAssetType assetTypeForExtension(const QString& extension) {
auto extensionLower = extension.toLower();
if (BAKEABLE_MODEL_EXTENSIONS.contains(extensionLower)) {
return Model;
} else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extensionLower.toLocal8Bit())) {
return Texture;
} else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extensionLower)) {
return Script;
}
return Undefined;
}
BakedAssetType assetTypeForFilename(const QString& filename) {
auto dotIndex = filename.lastIndexOf(".");
if (dotIndex == -1) {
return BakedAssetType::Undefined;
}
auto extension = filename.mid(dotIndex + 1);
return assetTypeForExtension(extension);
}
QString bakedFilenameForAssetType(BakedAssetType type) {
switch (type) {
case Model:
return BAKED_MODEL_SIMPLE_NAME;
case Texture:
return BAKED_TEXTURE_SIMPLE_NAME;
case Script:
return BAKED_SCRIPT_SIMPLE_NAME;
default:
return "";
}
}
BakeVersion currentBakeVersionForAssetType(BakedAssetType type) {
switch (type) {
case Model:
return (BakeVersion)CURRENT_MODEL_BAKE_VERSION;
case Texture:
return (BakeVersion)CURRENT_TEXTURE_BAKE_VERSION;
case Script:
return (BakeVersion)CURRENT_SCRIPT_BAKE_VERSION;
default:
return 0;
}
}
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
void AssetServer::bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath) {
qDebug() << "Starting bake for: " << assetPath << assetHash;
auto it = _pendingBakes.find(assetHash);
@ -167,36 +220,38 @@ bool AssetServer::needsToBeBaked(const AssetUtils::AssetPath& path, const AssetU
return false;
}
auto dotIndex = path.lastIndexOf(".");
if (dotIndex == -1) {
BakedAssetType type = assetTypeForFilename(path);
if (type == Undefined) {
return false;
}
auto extension = path.mid(dotIndex + 1);
QString bakedFilename = bakedFilenameForAssetType(type);
auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename;
auto mappingIt = _fileMappings.find(bakedPath);
bool bakedMappingExists = mappingIt != _fileMappings.end();
QString bakedFilename;
// If the path is mapped to the original file's hash, baking has been disabled for this
// asset
if (bakedMappingExists && mappingIt->second == assetHash) {
return false;
}
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(assetHash);
// TODO: Allow failed bakes that happened on old versions to be re-baked
if (loaded && meta.failedLastBake) {
if (type == Texture && !loaded) {
return false;
}
if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) {
bakedFilename = BAKED_MODEL_SIMPLE_NAME;
} else if (loaded && BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit())) {
bakedFilename = BAKED_TEXTURE_SIMPLE_NAME;
} else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) {
bakedFilename = BAKED_SCRIPT_SIMPLE_NAME;
} else {
auto currentVersion = currentBakeVersionForAssetType(type);
if (loaded && (meta.failedLastBake && meta.bakeVersion >= currentVersion)) {
return false;
}
auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename;
return _fileMappings.find(bakedPath) == _fileMappings.end();
return !bakedMappingExists || (meta.bakeVersion < currentVersion);
}
bool interfaceRunning() {
@ -598,15 +653,9 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketLi
// first, figure out from the mapping extension what type of file this is
auto assetPathExtension = assetPath.mid(assetPath.lastIndexOf('.') + 1).toLower();
QString bakedRootFile;
if (BAKEABLE_MODEL_EXTENSIONS.contains(assetPathExtension)) {
bakedRootFile = BAKED_MODEL_SIMPLE_NAME;
} else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(assetPathExtension.toLocal8Bit())) {
bakedRootFile = BAKED_TEXTURE_SIMPLE_NAME;
} else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(assetPathExtension)) {
bakedRootFile = BAKED_SCRIPT_SIMPLE_NAME;
}
auto type = assetTypeForFilename(assetPath);
QString bakedRootFile = bakedFilenameForAssetType(type);
auto originalAssetHash = it->second;
QString redirectedAssetHash;
@ -653,9 +702,19 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketLi
auto query = QUrlQuery(url.query());
bool isSkybox = query.hasQueryItem("skybox");
if (isSkybox) {
writeMetaFile(originalAssetHash);
if (!bakingDisabled) {
maybeBake(assetPath, originalAssetHash);
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(originalAssetHash);
if (!loaded) {
AssetMeta needsBakingMeta;
needsBakingMeta.bakeVersion = NEEDS_BAKING_BAKE_VERSION;
writeMetaFile(originalAssetHash, needsBakingMeta);
if (!bakingDisabled) {
maybeBake(assetPath, originalAssetHash);
}
}
}
}
@ -1275,15 +1334,19 @@ QString getBakeMapping(const AssetUtils::AssetHash& hash, const QString& relativ
}
void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, QString errors) {
qDebug() << "Failed: " << originalAssetHash << assetPath << errors;
qDebug() << "Failed to bake: " << originalAssetHash << assetPath << "(" << errors << ")";
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(originalAssetHash);
auto type = assetTypeForFilename(assetPath);
auto currentTypeVersion = currentBakeVersionForAssetType(type);
meta.failedLastBake = true;
meta.lastBakeErrors = errors;
meta.bakeVersion = currentTypeVersion;
writeMetaFile(originalAssetHash, meta);
@ -1373,17 +1436,20 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina
qWarning() << "Failed to remove temporary directory:" << bakedTempOutputDir;
}
if (!errorCompletingBake) {
// create the meta file to store which version of the baking process we just completed
writeMetaFile(originalAssetHash);
} else {
auto type = assetTypeForFilename(originalAssetPath);
auto currentTypeVersion = currentBakeVersionForAssetType(type);
AssetMeta meta;
meta.bakeVersion = currentTypeVersion;
meta.failedLastBake = errorCompletingBake;
if (errorCompletingBake) {
qWarning() << "Could not complete bake for" << originalAssetHash;
AssetMeta meta;
meta.failedLastBake = true;
meta.lastBakeErrors = errorReason;
writeMetaFile(originalAssetHash, meta);
}
writeMetaFile(originalAssetHash, meta);
_pendingBakes.remove(originalAssetHash);
}
@ -1447,7 +1513,7 @@ bool AssetServer::writeMetaFile(AssetUtils::AssetHash originalAssetHash, const A
// construct the JSON that will be in the meta file
QJsonObject metaFileObject;
metaFileObject[BAKE_VERSION_KEY] = meta.bakeVersion;
metaFileObject[BAKE_VERSION_KEY] = (int)meta.bakeVersion;
metaFileObject[FAILED_LAST_BAKE_KEY] = meta.failedLastBake;
metaFileObject[LAST_BAKE_ERRORS_KEY] = meta.lastBakeErrors;
@ -1479,27 +1545,13 @@ bool AssetServer::setBakingEnabled(const AssetUtils::AssetPathList& paths, bool
for (const auto& path : paths) {
auto it = _fileMappings.find(path);
if (it != _fileMappings.end()) {
auto type = assetTypeForFilename(path);
if (type == Undefined) {
continue;
}
QString bakedFilename = bakedFilenameForAssetType(type);
auto hash = it->second;
auto dotIndex = path.lastIndexOf(".");
if (dotIndex == -1) {
continue;
}
auto extension = path.mid(dotIndex + 1);
QString bakedFilename;
if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) {
bakedFilename = BAKED_MODEL_SIMPLE_NAME;
} else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(hash)) {
bakedFilename = BAKED_TEXTURE_SIMPLE_NAME;
} else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) {
bakedFilename = BAKED_SCRIPT_SIMPLE_NAME;
} else {
continue;
}
auto bakedMapping = getBakeMapping(hash, bakedFilename);
auto it = _fileMappings.find(bakedMapping);

View file

@ -23,8 +23,47 @@
#include "RegisteredMetaTypes.h"
using BakeVersion = int;
static const BakeVersion INITIAL_BAKE_VERSION = 0;
static const BakeVersion NEEDS_BAKING_BAKE_VERSION = -1;
enum BakedAssetType : int {
Model = 0,
Texture,
Script,
NUM_ASSET_TYPES,
Undefined
};
// ATTENTION! If you change the current version for an asset type, you will also
// need to update the function currentBakeVersionForAssetType() inside of AssetServer.cpp.
enum class ModelBakeVersion : BakeVersion {
Initial = INITIAL_BAKE_VERSION,
COUNT
};
// ATTENTION! See above.
enum class TextureBakeVersion : BakeVersion {
Initial = INITIAL_BAKE_VERSION,
COUNT
};
// ATTENTION! See above.
enum class ScriptBakeVersion : BakeVersion {
Initial = INITIAL_BAKE_VERSION,
FixEmptyScripts,
COUNT
};
struct AssetMeta {
int bakeVersion { 0 };
AssetMeta() {
}
BakeVersion bakeVersion;
bool failedLastBake { false };
QString lastBakeErrors;
};

View file

@ -132,6 +132,41 @@ $(document).ready(function(){
var ACTIVE_BACKUP_ROW_CLASS = 'active-backup';
var CORRUPTED_ROW_CLASS = 'danger';
$('body').on('click', '.' + BACKUP_DOWNLOAD_LINK_CLASS, function(ev) {
ev.preventDefault();
var backupID = $(this).data('backup-id');
showSpinnerAlert("Preparing backup...");
function checkBackupStatus() {
$.ajax({
url: "/api/backups/" + backupID,
dataType: 'json',
success: function(data) {
if (data.complete) {
if (data.error == '') {
location.href = "/api/backups/download/" + backupID;
swal.close();
} else {
showErrorMessage(
"Error",
"There was an error preparing your backup. Please refresh the page and try again."
);
}
} else {
setTimeout(checkBackupStatus, 500);
}
},
error: function() {
showErrorMessage(
"Error",
"There was an error preparing your backup."
);
},
});
}
checkBackupStatus();
});
function reloadBackupInformation() {
// make a GET request to get backup information to populate the table
$.ajax({
@ -164,7 +199,7 @@ $(document).ready(function(){
+ "<div class='dropdown'><div class='dropdown-toggle' data-toggle='dropdown' aria-expanded='false'><span class='glyphicon glyphicon-option-vertical'></span></div>"
+ "<ul class='dropdown-menu dropdown-menu-right'>"
+ "<li><a class='" + BACKUP_RESTORE_LINK_CLASS + "' href='#'>Restore from here</a></li><li class='divider'></li>"
+ "<li><a class='" + BACKUP_DOWNLOAD_LINK_CLASS + "' href='/api/backups/" + backup.id + "'>Download</a></li><li class='divider'></li>"
+ "<li><a class='" + BACKUP_DOWNLOAD_LINK_CLASS + "' data-backup-id='" + backup.id + "' href='#'>Download</a></li><li class='divider'></li>"
+ "<li><a class='" + BACKUP_DELETE_LINK_CLASS + "' href='#' target='_blank'>Delete</a></li></ul></div></td>";
}

View file

@ -2,7 +2,7 @@ if (typeof Settings === "undefined") {
Settings = {};
}
Object.assign(Settings, {
$.extend(Settings, {
DEPRECATED_CLASS: 'deprecated-setting',
TRIGGER_CHANGE_CLASS: 'trigger-change',
DATA_ROW_CLASS: 'value-row',

View file

@ -55,15 +55,20 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire
const QVariantList& backupRules,
std::chrono::milliseconds persistInterval,
bool debugTimestampNow) :
_consolidatedBackupDirectory(PathUtils::generateTemporaryDir()),
_backupDirectory(backupDirectory), _persistInterval(persistInterval), _lastCheck(p_high_resolution_clock::now())
{
setObjectName("DomainContentBackupManager");
// Make sure the backup directory exists.
QDir(_backupDirectory).mkpath(".");
parseBackupRules(backupRules);
constexpr int CONSOLIDATED_BACKUP_CLEANER_INTERVAL_MSECS = 30 * 1000;
_consolidatedBackupCleanupTimer.setInterval(CONSOLIDATED_BACKUP_CLEANER_INTERVAL_MSECS);
connect(&_consolidatedBackupCleanupTimer, &QTimer::timeout, this, &DomainContentBackupManager::removeOldConsolidatedBackups);
_consolidatedBackupCleanupTimer.start();
}
void DomainContentBackupManager::parseBackupRules(const QVariantList& backupRules) {
@ -498,23 +503,87 @@ void DomainContentBackupManager::backup() {
}
}
void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise, QString fileName) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "consolidateBackup", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(QString, fileName));
return;
void DomainContentBackupManager::removeOldConsolidatedBackups() {
constexpr std::chrono::minutes MAX_TIME_TO_KEEP_CONSOLIDATED_BACKUP { 30 };
auto now = std::chrono::system_clock::now();
auto it = _consolidatedBackups.begin();
while (it != _consolidatedBackups.end()) {
auto& backup = it->second;
auto diff = now - backup.createdAt;
if (diff > MAX_TIME_TO_KEEP_CONSOLIDATED_BACKUP) {
QFile oldBackup(backup.absoluteFilePath);
if (!oldBackup.exists() || oldBackup.remove()) {
qDebug() << "Removed old consolidated backup: " << backup.absoluteFilePath;
it = _consolidatedBackups.erase(it);
} else {
qDebug() << "Failed to remove old consolidated backup: " << backup.absoluteFilePath;
it++;
}
} else {
it++;
}
}
}
ConsolidatedBackupInfo DomainContentBackupManager::consolidateBackup(QString fileName) {
{
std::lock_guard<std::mutex> lock { _consolidatedBackupsMutex };
auto it = _consolidatedBackups.find(fileName);
if (it != _consolidatedBackups.end()) {
return it->second;
}
}
QMetaObject::invokeMethod(this, "consolidateBackupInternal", Q_ARG(QString, fileName));
return {
ConsolidatedBackupInfo::CONSOLIDATING,
"",
"",
std::chrono::system_clock::now()
};
}
void DomainContentBackupManager::consolidateBackupInternal(QString fileName) {
auto markFailure = [this, &fileName](QString error) {
qWarning() << "Failed to consolidate backup:" << fileName << error;
{
std::lock_guard<std::mutex> lock { _consolidatedBackupsMutex };
auto& consolidatedBackup = _consolidatedBackups[fileName];
consolidatedBackup.state = ConsolidatedBackupInfo::COMPLETE_WITH_ERROR;
consolidatedBackup.error = error;
}
};
{
std::lock_guard<std::mutex> lock { _consolidatedBackupsMutex };
auto it = _consolidatedBackups.find(fileName);
if (it != _consolidatedBackups.end()) {
return;
}
_consolidatedBackups[fileName] = {
ConsolidatedBackupInfo::CONSOLIDATING,
"",
"",
std::chrono::system_clock::now()
};
}
QDir backupDir { _backupDirectory };
if (!backupDir.exists()) {
qCritical() << "Backup directory does not exist, bailing consolidation of backup";
promise->resolve({ { "success", false } });
markFailure("Backup directory does not exist, bailing consolidation of backup");
return;
}
auto filePath = backupDir.absoluteFilePath(fileName);
if (!QFile::exists(filePath)) {
markFailure("Backup does not exist");
return;
}
auto copyFilePath = QDir::tempPath() + "/" + fileName;
auto copyFilePath = _consolidatedBackupDirectory + "/" + fileName;
{
QFile copyFile(copyFilePath);
@ -523,8 +592,7 @@ void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise,
}
auto copySuccess = QFile::copy(filePath, copyFilePath);
if (!copySuccess) {
qCritical() << "Failed to create copy of backup.";
promise->resolve({ { "success", false } });
markFailure("Failed to create copy of backup.");
return;
}
@ -532,7 +600,7 @@ void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise,
if (!zip.open(QuaZip::mdAdd)) {
qCritical() << "Could not open backup archive:" << filePath;
qCritical() << " ERROR:" << zip.getZipError();
promise->resolve({ { "success", false } });
markFailure("Could not open backup archive");
return;
}
@ -544,14 +612,17 @@ void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise,
if (zip.getZipError() != UNZ_OK) {
qCritical() << "Failed to consolidate backup: " << zip.getZipError();
promise->resolve({ { "success", false } });
markFailure("Failed to consolidate backup");
return;
}
promise->resolve({
{ "success", true },
{ "backupFilePath", copyFilePath }
});
{
std::lock_guard<std::mutex> lock { _consolidatedBackupsMutex };
auto& consolidatedBackup = _consolidatedBackups[fileName];
consolidatedBackup.state = ConsolidatedBackupInfo::COMPLETE_WITH_SUCCESS;
consolidatedBackup.absoluteFilePath = copyFilePath;
}
}
void DomainContentBackupManager::createManualBackup(MiniPromise::Promise promise, const QString& name) {

View file

@ -15,9 +15,15 @@
#ifndef hifi_DomainContentBackupManager_h
#define hifi_DomainContentBackupManager_h
#include <RegisteredMetaTypes.h>
#include <QString>
#include <QVector>
#include <QDateTime>
#include <QTimer>
#include <mutex>
#include <unordered_map>
#include <GenericThread.h>
@ -38,6 +44,18 @@ struct BackupItemInfo {
bool isManualBackup;
};
struct ConsolidatedBackupInfo {
enum State {
CONSOLIDATING,
COMPLETE_WITH_ERROR,
COMPLETE_WITH_SUCCESS
};
State state;
QString error;
QString absoluteFilePath;
std::chrono::system_clock::time_point createdAt;
};
class DomainContentBackupManager : public GenericThread {
Q_OBJECT
public:
@ -61,6 +79,7 @@ public:
void addBackupHandler(BackupHandlerPointer handler);
void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist
void replaceData(QByteArray data);
ConsolidatedBackupInfo consolidateBackup(QString fileName);
public slots:
void getAllBackupsAndStatus(MiniPromise::Promise promise);
@ -68,7 +87,6 @@ public slots:
void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName);
void recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup);
void deleteBackup(MiniPromise::Promise promise, const QString& backupName);
void consolidateBackup(MiniPromise::Promise promise, QString fileName);
signals:
void loadCompleted();
@ -91,11 +109,21 @@ protected:
bool recoverFromBackupZip(const QString& backupName, QuaZip& backupZip);
private slots:
void removeOldConsolidatedBackups();
void consolidateBackupInternal(QString fileName);
private:
QTimer _consolidatedBackupCleanupTimer;
const QString _consolidatedBackupDirectory;
const QString _backupDirectory;
std::vector<BackupHandlerPointer> _backupHandlers;
std::chrono::milliseconds _persistInterval { 0 };
std::mutex _consolidatedBackupsMutex;
std::unordered_map<QString, ConsolidatedBackupInfo> _consolidatedBackups;
std::atomic<bool> _isRecovering { false };
QString _recoveryFilename { };

View file

@ -164,6 +164,8 @@ DomainServer::DomainServer(int argc, char* argv[]) :
_iceServerAddr(ICE_SERVER_DEFAULT_HOSTNAME),
_iceServerPort(ICE_SERVER_DEFAULT_PORT)
{
PathUtils::removeTemporaryApplicationDirs();
parseCommandLine();
DependencyManager::set<tracing::Tracer>();
@ -1933,6 +1935,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString URI_API_DOMAINS_ID = "/api/domains/";
const QString URI_API_BACKUPS = "/api/backups";
const QString URI_API_BACKUPS_ID = "/api/backups/";
const QString URI_API_BACKUPS_DOWNLOAD_ID = "/api/backups/download/";
const QString URI_API_BACKUPS_RECOVER = "/api/backups/recover/";
const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
@ -2133,37 +2136,40 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
});
_contentManager->getAllBackupsAndStatus(deferred);
return true;
} else if (url.path().startsWith(URI_API_BACKUPS_DOWNLOAD_ID)) {
auto id = url.path().mid(QString(URI_API_BACKUPS_DOWNLOAD_ID).length());
auto info = _contentManager->consolidateBackup(id);
if (info.state == ConsolidatedBackupInfo::COMPLETE_WITH_SUCCESS) {
auto file { std::unique_ptr<QFile>(new QFile(info.absoluteFilePath)) };
if (file->open(QIODevice::ReadOnly)) {
constexpr const char* CONTENT_TYPE_ZIP = "application/zip";
auto downloadedFilename = id;
downloadedFilename.replace(QRegularExpression(".zip$"), ".content.zip");
auto contentDisposition = "attachment; filename=\"" + downloadedFilename + "\"";
connectionPtr->respond(HTTPConnection::StatusCode200, std::move(file), CONTENT_TYPE_ZIP, {
{ "Content-Disposition", contentDisposition.toUtf8() }
});
} else {
qCritical(domain_server) << "Unable to load consolidated backup at:" << info.absoluteFilePath;
connectionPtr->respond(HTTPConnection::StatusCode500, "Error opening backup");
}
} else if (info.state == ConsolidatedBackupInfo::COMPLETE_WITH_ERROR) {
connectionPtr->respond(HTTPConnection::StatusCode500, ("Error creating backup: " + info.error).toUtf8());
} else {
connectionPtr->respond(HTTPConnection::StatusCode400, "Backup unavailable");
}
return true;
} else if (url.path().startsWith(URI_API_BACKUPS_ID)) {
auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length());
auto deferred = makePromise("consolidateBackup");
deferred->then([connectionPtr, JSON_MIME_TYPE, id](QString error, QVariantMap result) {
if (!connectionPtr) {
return;
}
auto info = _contentManager->consolidateBackup(id);
QJsonObject rootJSON;
auto success = result["success"].toBool();
if (success) {
auto path = result["backupFilePath"].toString();
auto file { std::unique_ptr<QFile>(new QFile(path)) };
if (file->open(QIODevice::ReadOnly)) {
constexpr const char* CONTENT_TYPE_ZIP = "application/zip";
auto downloadedFilename = id;
downloadedFilename.replace(QRegularExpression(".zip$"), ".content.zip");
auto contentDisposition = "attachment; filename=\"" + downloadedFilename + "\"";
connectionPtr->respond(HTTPConnection::StatusCode200, std::move(file), CONTENT_TYPE_ZIP, {
{ "Content-Disposition", contentDisposition.toUtf8() }
});
} else {
qCritical(domain_server) << "Unable to load consolidated backup at:" << path << result;
connectionPtr->respond(HTTPConnection::StatusCode500, "Error opening backup");
}
} else {
connectionPtr->respond(HTTPConnection::StatusCode400);
}
});
_contentManager->consolidateBackup(deferred, id);
QJsonObject rootJSON {
{ "complete", info.state == ConsolidatedBackupInfo::COMPLETE_WITH_SUCCESS },
{ "error", info.error }
};
QJsonDocument docJSON { rootJSON };
connectionPtr->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8());
return true;
} else if (url.path() == URI_RESTART) {
@ -2216,7 +2222,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString ASSIGNMENT_INSTANCES_HEADER = "ASSIGNMENT-INSTANCES";
const QString ASSIGNMENT_POOL_HEADER = "ASSIGNMENT-POOL";
QByteArray assignmentInstancesValue = connection->requestHeaders().value(ASSIGNMENT_INSTANCES_HEADER.toLocal8Bit());
QByteArray assignmentInstancesValue = connection->requestHeader(ASSIGNMENT_INSTANCES_HEADER.toLocal8Bit());
int numInstances = 1;
@ -2228,7 +2234,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
}
QString assignmentPool = emptyPool;
QByteArray assignmentPoolValue = connection->requestHeaders().value(ASSIGNMENT_POOL_HEADER.toLocal8Bit());
QByteArray assignmentPoolValue = connection->requestHeader(ASSIGNMENT_POOL_HEADER.toLocal8Bit());
if (!assignmentPoolValue.isEmpty()) {
// specific pool requested, set that on the created assignment
@ -2626,7 +2632,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
if (!_oauthProviderURL.isEmpty()
&& (adminUsersVariant.isValid() || adminRolesVariant.isValid())) {
QString cookieString = connection->requestHeaders().value(HTTP_COOKIE_HEADER_KEY);
QString cookieString = connection->requestHeader(HTTP_COOKIE_HEADER_KEY);
const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)";
QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING);
@ -2671,7 +2677,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
static const QByteArray REQUESTED_WITH_HEADER = "X-Requested-With";
static const QString XML_REQUESTED_WITH = "XMLHttpRequest";
if (connection->requestHeaders().value(REQUESTED_WITH_HEADER) == XML_REQUESTED_WITH) {
if (connection->requestHeader(REQUESTED_WITH_HEADER) == XML_REQUESTED_WITH) {
// unauthorized XHR requests get a 401 and not a 302, since there isn't an XHR
// path to OAuth authorize
connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY);
@ -2702,7 +2708,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization";
// check if a username and password have been provided with the request
QString basicAuthString = connection->requestHeaders().value(BASIC_AUTH_HEADER_KEY);
QString basicAuthString = connection->requestHeader(BASIC_AUTH_HEADER_KEY);
if (!basicAuthString.isEmpty()) {
QStringList splitAuthString = basicAuthString.split(' ');

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<path d="M45,40.3l-7.1-6.3c-0.7-0.7-1.7-0.9-2.6-0.8c2.3-2.9,3.6-6.4,3.6-10.3c0-9.9-7.9-17-16.9-17.1c-9.3,0-16.7,7.4-17,16.3
C4.7,32,12.6,39.6,21.9,39.7c4.2,0,8.1-1.5,11-4.1c-0.2,0.9,0.1,1.9,0.9,2.6l7.1,6.3c1.2,1.1,3,1.1,4.1-0.1c0.5-0.6,0.8-1.3,0.8-2
C45.9,41.6,45.6,40.8,45,40.3z M22.5,35.4c-7.3,0.3-13.2-5.5-13.2-12.8C9.2,15.8,14.9,10.1,21.9,10c7-0.1,12.8,5.6,12.8,12.7
C34.7,29.7,29.2,35.2,22.5,35.4z"/>
<path d="M26.2,19.9c-0.5,1.2-0.9,2.4-1.3,3.5c-0.6,1.6-1.2,3.2-1.8,4.8c-0.3,0.8-0.8,1.2-1.6,1.2c-0.8,0-1.2-0.3-1.6-1.2
c-0.9-2.1-1.8-4.2-2.6-6.2c0-0.1-0.1-0.2-0.2-0.5c-0.3,0.5-0.6,0.9-0.8,1.4c-0.4,0.9-1.1,1.3-2.1,1.2c-0.6,0-1.2,0-1.8,0
c-0.9,0-1.5-0.6-1.5-1.4c-0.1-0.7,0.5-1.4,1.3-1.5c0.2,0,0.4-0.1,0.5-0.1c0.7,0.1,1.1-0.2,1.5-0.9c0.5-1,1.1-2,1.6-3
c0.3-0.6,0.8-1,1.6-1c0.7,0,1.2,0.5,1.5,1.1c0.8,1.8,1.6,3.7,2.3,5.5c0.1,0.2,0.1,0.3,0.3,0.6c0.2-0.6,0.4-1.1,0.6-1.6
c0.9-2.4,1.8-4.8,2.7-7.2c0.3-0.9,0.8-1.2,1.5-1.2c0.8,0,1.3,0.4,1.6,1.2c0.7,1.8,1.3,3.6,1.9,5.5c0.4,1,0.1,0.8,1.1,0.8
c0.4,0,0.8,0,1.1,0.1c0.7,0.2,1.2,0.8,1.1,1.6c-0.1,0.7-0.6,1.3-1.4,1.3c-1,0-2,0-3,0c-0.7,0-1.1-0.4-1.4-1.1
C27,22.1,26.6,21.1,26.2,19.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#EF3B4E;}
</style>
<circle class="st0" cx="44.1" cy="6" r="5.6"/>
<g>
<path d="M17.6,33.4c0.4,0,0.6,0,0.9,0c6.5,0,13,0,19.5,0c1.3,0,2,0.5,2.2,1.5c0.2,1.1-0.5,2.2-1.6,2.3c-0.2,0-0.5,0-0.7,0
c-7.4,0-14.8,0-22.3,0c-2,0-2.7-0.9-2.2-2.9c0.3-1.1,0.5-2.3,0.8-3.4c0.3-0.9,0.2-1.8,0-2.8C12.7,22,11.1,15.9,9.6,9.8
C9.4,9.3,9.2,9.1,8.7,9.1c-1.3,0-2.6,0-3.9,0c-1.2,0-2-0.8-2-1.9c0-1.1,0.8-1.9,2-1.9c2,0,3.9,0,5.9,0c1.1,0,1.7,0.5,2,1.5
c0.4,1.3,1.6,4.8,1.9,6.2c2.2,0.1,4.3,0.2,6.5,0.3c6.7,0.3,11.4,0.4,18.1,0.7c1.2,0.1,2.3,0.1,3.5,0.2c1.4,0.1,2.2,1.4,1.6,2.6
c-0.9,2.5-3.5,8.5-4.4,10.4c-0.4,0.8-1,1.1-1.9,1.2c-5.9,0.3-12.5,0.5-18.4,0.7c-0.8,0-1.2,0.3-1.3,1.1
C18.4,30.7,17.7,32.9,17.6,33.4z"/>
<path d="M39.2,42c0,1.9-1.6,3.6-3.5,3.6c-2,0-3.6-1.6-3.6-3.6c0-1.9,1.6-3.6,3.6-3.6C37.6,38.5,39.2,40.1,39.2,42z"/>
<path d="M17.8,41.9c0,1.9-1.7,3.5-3.6,3.5c-1.9,0-3.5-1.7-3.5-3.6c0-2,1.6-3.6,3.6-3.6C16.2,38.3,17.8,39.9,17.8,41.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,64 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="market-a.svg"><metadata
id="metadata20"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs18" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview16"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><g
id="g8"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M45.4,13.7c-1.6-0.1-3.2,0-4.8,0c-7.5,0-18,0-25.7,0c-0.6-1.8-1-3.5-1.7-5.2C13,7.9,12.2,7.1,11.5,7 C9.4,6.7,7.3,6.7,5.2,6.7c-1.1,0-1.9,0.5-1.9,1.7c0,1.2,0.8,1.7,1.9,1.7c1.3,0,2.7,0,4,0.1c0.5,0.1,1.2,0.6,1.4,1.1 c0.9,2.6,1.7,5.2,2.5,7.8c1.3,4.3,1.8,5.5,3.1,10.2c0.6,2.3,1.2,2.8,2.2,3.3c1.1,0.4,2.1,0.4,2.1,0.4h1.8c4.6,0,12.2,0,16.8,0 c1.1,0,2.1-0.1,2.6-1.4c1.9-5.1,3.8-10.2,5.7-15.3C47.8,14.7,47.1,13.8,45.4,13.7z M38.9,28.7c-0.1,0.3-0.8,0.7-1.2,0.7 c-4.6,0-12.2,0-16.8,0c-0.4,0-1.1-0.3-1.2-0.7c-1.3-3.8-2.4-7.6-3.7-11.5h27.1C41.8,21.2,40.4,24.9,38.9,28.7z"
id="path10"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M17.2,37.3L17.2,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.3-0.9,2.3c0,1,0.3,1.8,0.8,2.4 c0.6,0.6,1.4,0.9,2.2,0.9c1.8,0,3.2-1.4,3.2-3.2c0-0.9-0.3-1.7-0.9-2.3C18.8,37.6,18,37.3,17.2,37.3z"
id="path12"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M35.1,37.3L35.1,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.4-0.9,2.4c0,1,0.3,1.8,0.8,2.4 c0.6,0.6,1.3,0.9,2.2,0.9c1.8,0,3.2-1.5,3.2-3.3c0-0.9-0.3-1.6-0.9-2.2C36.8,37.6,36,37.3,35.1,37.3z"
id="path14"
style="fill:#000000;fill-opacity:1" /></g></g></svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<g>
<path d="M17.6,33.4c0.4,0,0.6,0,0.9,0c6.5,0,13,0,19.5,0c1.3,0,2,0.5,2.2,1.5c0.2,1.1-0.5,2.2-1.6,2.3c-0.2,0-0.5,0-0.7,0
c-7.4,0-14.8,0-22.3,0c-2,0-2.7-0.9-2.2-2.9c0.3-1.1,0.5-2.3,0.8-3.4c0.3-0.9,0.2-1.8,0-2.8C12.7,22,11.1,15.9,9.6,9.8
C9.4,9.3,9.2,9.1,8.7,9.1c-1.3,0-2.6,0-3.9,0c-1.2,0-2-0.8-2-1.9c0-1.1,0.8-1.9,2-1.9c2,0,3.9,0,5.9,0c1.1,0,1.7,0.5,2,1.5
c0.4,1.3,1.6,4.8,1.9,6.2c2.2,0.1,4.3,0.2,6.5,0.3c6.7,0.3,11.4,0.4,18.1,0.7c1.2,0.1,2.3,0.1,3.5,0.2c1.4,0.1,2.2,1.4,1.6,2.6
c-0.9,2.5-3.5,8.5-4.4,10.4c-0.4,0.8-1,1.1-1.9,1.2c-5.9,0.3-12.5,0.5-18.4,0.7c-0.8,0-1.2,0.3-1.3,1.1
C18.4,30.7,17.7,32.9,17.6,33.4z"/>
<path d="M39.2,42c0,1.9-1.6,3.6-3.5,3.6c-2,0-3.6-1.6-3.6-3.6c0-1.9,1.6-3.6,3.6-3.6C37.6,38.5,39.2,40.1,39.2,42z"/>
<path d="M17.8,41.9c0,1.9-1.7,3.5-3.6,3.5c-1.9,0-3.5-1.7-3.5-3.6c0-2,1.6-3.6,3.6-3.6C16.2,38.3,17.8,39.9,17.8,41.9z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#EF3B4E;}
.st1{fill:#FFFFFF;}
</style>
<circle class="st0" cx="44.1" cy="6" r="5.6"/>
<g>
<path class="st1" d="M17.6,33.4c0.4,0,0.6,0,0.9,0c6.5,0,13,0,19.5,0c1.3,0,2,0.5,2.2,1.5c0.2,1.1-0.5,2.2-1.6,2.3
c-0.2,0-0.5,0-0.7,0c-7.4,0-14.8,0-22.3,0c-2,0-2.7-0.9-2.2-2.9c0.3-1.1,0.5-2.3,0.8-3.4c0.3-0.9,0.2-1.8,0-2.8
C12.7,22,11.1,15.9,9.6,9.8C9.4,9.3,9.2,9.1,8.7,9.1c-1.3,0-2.6,0-3.9,0c-1.2,0-2-0.8-2-1.9c0-1.1,0.8-1.9,2-1.9c2,0,3.9,0,5.9,0
c1.1,0,1.7,0.5,2,1.5c0.4,1.3,1.6,4.8,1.9,6.2c2.2,0.1,4.3,0.2,6.5,0.3c6.7,0.3,11.4,0.4,18.1,0.7c1.2,0.1,2.3,0.1,3.5,0.2
c1.4,0.1,2.2,1.4,1.6,2.6c-0.9,2.5-3.5,8.5-4.4,10.4c-0.4,0.8-1,1.1-1.9,1.2c-5.9,0.3-12.5,0.5-18.4,0.7c-0.8,0-1.2,0.3-1.3,1.1
C18.4,30.7,17.7,32.9,17.6,33.4z"/>
<path class="st1" d="M39.2,42c0,1.9-1.6,3.6-3.5,3.6c-2,0-3.6-1.6-3.6-3.6c0-1.9,1.6-3.6,3.6-3.6C37.6,38.5,39.2,40.1,39.2,42z"/>
<path class="st1" d="M17.8,41.9c0,1.9-1.7,3.5-3.6,3.5c-1.9,0-3.5-1.7-3.5-3.6c0-2,1.6-3.6,3.6-3.6C16.2,38.3,17.8,39.9,17.8,41.9z
"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,23 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<g>
<path class="st0" d="M45.4,13.7c-1.6-0.1-3.2,0-4.8,0c-7.5,0-18,0-25.7,0c-0.6-1.8-1-3.5-1.7-5.2C13,7.9,12.2,7.1,11.5,7
C9.4,6.7,7.3,6.7,5.2,6.7c-1.1,0-1.9,0.5-1.9,1.7c0,1.2,0.8,1.7,1.9,1.7c1.3,0,2.7,0,4,0.1c0.5,0.1,1.2,0.6,1.4,1.1
c0.9,2.6,1.7,5.2,2.5,7.8c1.3,4.3,1.8,5.5,3.1,10.2c0.6,2.3,1.2,2.8,2.2,3.3c1.1,0.4,2.1,0.4,2.1,0.4h1.8c4.6,0,12.2,0,16.8,0
c1.1,0,2.1-0.1,2.6-1.4c1.9-5.1,3.8-10.2,5.7-15.3C47.8,14.7,47.1,13.8,45.4,13.7z M38.9,28.7c-0.1,0.3-0.8,0.7-1.2,0.7
c-4.6,0-12.2,0-16.8,0c-0.4,0-1.1-0.3-1.2-0.7c-1.3-3.8-2.4-7.6-3.7-11.5h27.1C41.8,21.2,40.4,24.9,38.9,28.7z"/>
<path class="st0" d="M17.2,37.3L17.2,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.3-0.9,2.3c0,1,0.3,1.8,0.8,2.4
c0.6,0.6,1.4,0.9,2.2,0.9c1.8,0,3.2-1.4,3.2-3.2c0-0.9-0.3-1.7-0.9-2.3C18.8,37.6,18,37.3,17.2,37.3z"/>
<path class="st0" d="M35.1,37.3L35.1,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.4-0.9,2.4c0,1,0.3,1.8,0.8,2.4
c0.6,0.6,1.3,0.9,2.2,0.9c1.8,0,3.2-1.5,3.2-3.3c0-0.9-0.3-1.6-0.9-2.2C36.8,37.6,36,37.3,35.1,37.3z"/>
</g>
<g>
<path class="st0" d="M17.6,33.4c0.4,0,0.6,0,0.9,0c6.5,0,13,0,19.5,0c1.3,0,2,0.5,2.2,1.5c0.2,1.1-0.5,2.2-1.6,2.3
c-0.2,0-0.5,0-0.7,0c-7.4,0-14.8,0-22.3,0c-2,0-2.7-0.9-2.2-2.9c0.3-1.1,0.5-2.3,0.8-3.4c0.3-0.9,0.2-1.8,0-2.8
C12.7,22,11.1,15.9,9.6,9.8C9.4,9.3,9.2,9.1,8.7,9.1c-1.3,0-2.6,0-3.9,0c-1.2,0-2-0.8-2-1.9c0-1.1,0.8-1.9,2-1.9c2,0,3.9,0,5.9,0
c1.1,0,1.7,0.5,2,1.5c0.4,1.3,1.6,4.8,1.9,6.2c2.2,0.1,4.3,0.2,6.5,0.3c6.7,0.3,11.4,0.4,18.1,0.7c1.2,0.1,2.3,0.1,3.5,0.2
c1.4,0.1,2.2,1.4,1.6,2.6c-0.9,2.5-3.5,8.5-4.4,10.4c-0.4,0.8-1,1.1-1.9,1.2c-5.9,0.3-12.5,0.5-18.4,0.7c-0.8,0-1.2,0.3-1.3,1.1
C18.4,30.7,17.7,32.9,17.6,33.4z"/>
<path class="st0" d="M39.2,42c0,1.9-1.6,3.6-3.5,3.6c-2,0-3.6-1.6-3.6-3.6c0-1.9,1.6-3.6,3.6-3.6C37.6,38.5,39.2,40.1,39.2,42z"/>
<path class="st0" d="M17.8,41.9c0,1.9-1.7,3.5-3.6,3.5c-1.9,0-3.5-1.7-3.5-3.6c0-2,1.6-3.6,3.6-3.6C16.2,38.3,17.8,39.9,17.8,41.9z
"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,9 @@
import QtQuick 2.3
import QtQuick.Controls 1.2
Text {
color: "white";
style: Text.Outline;
styleColor: "black";
font.pixelSize: 15;
}

View file

@ -0,0 +1,321 @@
//
// FilterBar.qml
//
// Created by Zach Fox on 17 Feb 2018-03-12
// Copyright 2018 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
//
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtGraphicalEffects 1.0
import "../styles-uit"
import "../controls-uit" as HifiControls
Item {
id: root;
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
readonly property bool isFaintGrayColorScheme: colorScheme == hifi.colorSchemes.faintGray
property bool error: false;
property alias textFieldHeight: textField.height;
property string placeholderText;
property alias dropdownHeight: dropdownContainer.height;
property alias text: textField.text;
property alias primaryFilterChoices: filterBarModel;
property int primaryFilter_index: -1;
property string primaryFilter_filterName: "";
property string primaryFilter_displayName: "";
signal accepted;
onPrimaryFilter_indexChanged: {
if (primaryFilter_index === -1) {
primaryFilter_filterName = "";
primaryFilter_displayName = "";
} else {
primaryFilter_filterName = filterBarModel.get(primaryFilter_index).filterName;
primaryFilter_displayName = filterBarModel.get(primaryFilter_index).displayName;
}
}
TextField {
id: textField;
anchors.top: parent.top;
anchors.right: parent.right;
anchors.left: parent.left;
font.family: "Fira Sans"
font.pixelSize: hifi.fontSizes.textFieldInput;
placeholderText: root.primaryFilter_index === -1 ? root.placeholderText : "";
TextMetrics {
id: primaryFilterTextMetrics;
font.family: "FiraSans Regular";
font.pixelSize: hifi.fontSizes.textFieldInput;
font.capitalization: Font.AllUppercase;
text: root.primaryFilter_displayName;
}
// workaround for https://bugreports.qt.io/browse/QTBUG-49297
Keys.onPressed: {
switch (event.key) {
case Qt.Key_Return:
case Qt.Key_Enter:
event.accepted = true;
// emit accepted signal manually
if (acceptableInput) {
root.accepted();
root.forceActiveFocus();
}
break;
case Qt.Key_Backspace:
if (textField.text === "") {
primaryFilter_index = -1;
}
break;
}
}
onAccepted: {
root.forceActiveFocus();
}
onActiveFocusChanged: {
if (!activeFocus) {
dropdownContainer.visible = false;
}
}
color: {
if (isLightColorScheme) {
if (textField.activeFocus) {
hifi.colors.black
} else {
hifi.colors.lightGray
}
} else if (isFaintGrayColorScheme) {
if (textField.activeFocus) {
hifi.colors.black
} else {
hifi.colors.lightGray
}
} else {
if (textField.activeFocus) {
hifi.colors.white
} else {
hifi.colors.lightGrayText
}
}
}
background: Rectangle {
id: mainFilterBarRectangle;
color: {
if (isLightColorScheme) {
if (textField.activeFocus) {
hifi.colors.white
} else {
hifi.colors.textFieldLightBackground
}
} else if (isFaintGrayColorScheme) {
if (textField.activeFocus) {
hifi.colors.white
} else {
hifi.colors.faintGray50
}
} else {
if (textField.activeFocus) {
hifi.colors.black
} else {
hifi.colors.baseGrayShadow
}
}
}
border.color: textField.error ? hifi.colors.redHighlight :
(textField.activeFocus ? hifi.colors.primaryHighlight : (isFaintGrayColorScheme ? hifi.colors.lightGrayText : hifi.colors.lightGray))
border.width: 1
radius: 4
Item {
id: searchButtonContainer;
anchors.left: parent.left;
anchors.verticalCenter: parent.verticalCenter;
height: parent.height;
width: 42;
// Search icon
HiFiGlyphs {
id: searchIcon;
text: hifi.glyphs.search
color: textField.color
size: 40;
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: paintedWidth;
}
// Carat
HiFiGlyphs {
text: hifi.glyphs.caratDn;
color: textField.color;
size: 40;
anchors.left: parent.left;
anchors.leftMargin: 15;
width: paintedWidth;
}
MouseArea {
anchors.fill: parent;
onClicked: {
textField.forceActiveFocus();
dropdownContainer.visible = !dropdownContainer.visible;
}
}
}
Rectangle {
z: 999;
id: primaryFilterContainer;
color: textField.activeFocus ? hifi.colors.faintGray : hifi.colors.white;
width: primaryFilterTextMetrics.tightBoundingRect.width + 14;
height: parent.height - 8;
anchors.verticalCenter: parent.verticalCenter;
anchors.left: searchButtonContainer.right;
anchors.leftMargin: 4;
visible: primaryFilterText.text !== "";
radius: height/2;
FiraSansRegular {
id: primaryFilterText;
text: root.primaryFilter_displayName;
anchors.fill: parent;
color: textField.activeFocus ? hifi.colors.black : hifi.colors.lightGray;
horizontalAlignment: Text.AlignHCenter;
verticalAlignment: Text.AlignVCenter;
size: hifi.fontSizes.textFieldInput;
font.capitalization: Font.AllUppercase;
}
MouseArea {
anchors.fill: parent;
onClicked: {
textField.forceActiveFocus();
}
}
}
// "Clear" button
HiFiGlyphs {
text: hifi.glyphs.error
color: textField.color
size: 40
anchors.right: parent.right
anchors.rightMargin: hifi.dimensions.textPadding - 2
anchors.verticalCenter: parent.verticalCenter
visible: root.text !== "" || root.primaryFilter_index !== -1;
MouseArea {
anchors.fill: parent;
onClicked: {
root.text = "";
root.primaryFilter_index = -1;
dropdownContainer.visible = false;
textField.forceActiveFocus();
}
}
}
}
selectedTextColor: hifi.colors.black
selectionColor: hifi.colors.primaryHighlight
leftPadding: 44 + (root.primaryFilter_index === -1 ? 0 : primaryFilterTextMetrics.tightBoundingRect.width + 20);
rightPadding: 44;
}
Rectangle {
id: dropdownContainer;
visible: false;
height: 50 * filterBarModel.count;
width: parent.width;
anchors.top: textField.bottom;
anchors.left: parent.left;
anchors.right: parent.right;
color: hifi.colors.white;
ListModel {
id: filterBarModel;
}
ListView {
id: dropdownListView;
interactive: false;
anchors.fill: parent;
model: filterBarModel;
delegate: Rectangle {
id: dropDownButton;
color: hifi.colors.white;
width: parent.width;
height: 50;
RalewaySemiBold {
id: dropDownButtonText;
text: model.displayName;
anchors.fill: parent;
anchors.leftMargin: 12;
color: hifi.colors.baseGray;
horizontalAlignment: Text.AlignLeft;
verticalAlignment: Text.AlignVCenter;
size: 18;
}
MouseArea {
anchors.fill: parent;
hoverEnabled: true;
propagateComposedEvents: false;
onEntered: {
dropDownButton.color = hifi.colors.blueHighlight;
}
onExited: {
dropDownButton.color = hifi.colors.white;
}
onClicked: {
textField.forceActiveFocus();
root.primaryFilter_index = index;
dropdownContainer.visible = false;
}
}
}
}
}
DropShadow {
anchors.fill: dropdownContainer;
horizontalOffset: 0;
verticalOffset: 4;
radius: 4.0;
samples: 9
color: Qt.rgba(0, 0, 0, 0.25);
source: dropdownContainer;
visible: dropdownContainer.visible;
}
function changeFilterByDisplayName(name) {
for (var i = 0; i < filterBarModel.count; i++) {
if (filterBarModel.get(i).displayName === name) {
root.primaryFilter_index = i;
return;
}
}
console.log("Passed displayName not found in filterBarModel! primaryFilter unchanged.");
}
}

View file

@ -163,8 +163,10 @@ TextField {
text: textField.label
colorScheme: textField.colorScheme
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.top
anchors.bottomMargin: 3
wrapMode: Text.WordWrap
visible: label != ""
}
}

View file

@ -65,34 +65,33 @@ TabletModalWindow {
id: modalWindowItem
width: parent.width - 12
height: 240
anchors {
verticalCenter: parent.verticalCenter
horizontalCenter: parent.horizontalCenter
}
anchors.horizontalCenter: parent.horizontalCenter
QtObject {
id: d
readonly property int minWidth: 470
readonly property int maxWidth: 470
readonly property int minWidth: modalWindowItem.width
readonly property int maxWidth: modalWindowItem.width
readonly property int minHeight: 120
readonly property int maxHeight: 720
function resize() {
var targetWidth = Math.max(titleWidth, 470)
var targetWidth = Math.max(titleWidth, modalWindowItem.width)
var targetHeight = (items ? comboBox.controlHeight : textResult.controlHeight) + 5 * hifi.dimensions.contentSpacing.y + buttons.height
modalWindowItem.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth);
modalWindowItem.height = ((targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + 2 * hifi.dimensions.contentSpacing.y) : 0) + modalWindowItem.frameMarginTop
modalWindowItem.height = ((targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + modalWindowItem.frameMarginTop
modalWindowItem.y = (root.height - (modalWindowItem.height + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + 2 * hifi.dimensions.contentSpacing.y) : 0))) / 2
}
}
Item {
anchors {
top: parent.top
bottom: keyboard.top;
bottom: buttons.top;
left: parent.left;
right: parent.right;
margins: 0
bottomMargin: 2 * hifi.dimensions.contentSpacing.y
topMargin: modalWindowItem.frameMarginTop
}
// FIXME make a text field type that can be bound to a history for autocompletion
@ -106,6 +105,7 @@ TabletModalWindow {
right: parent.right;
bottom: parent.bottom
leftMargin: 5
rightMargin: 5
}
}
@ -124,22 +124,6 @@ TabletModalWindow {
}
}
property alias keyboardOverride: root.keyboardOverride
property alias keyboardRaised: root.keyboardRaised
property alias punctuationMode: root.punctuationMode
Keyboard {
id: keyboard
raised: keyboardEnabled && keyboardRaised
numeric: punctuationMode
anchors {
left: parent.left
right: parent.right
bottom: buttons.top
bottomMargin: raised ? 2 * hifi.dimensions.contentSpacing.y : 0
}
}
Flow {
id: buttons
focus: true
@ -150,6 +134,7 @@ TabletModalWindow {
bottom: parent.bottom
right: parent.right
margins: 0
rightMargin: hifi.dimensions.borderRadius
bottomMargin: hifi.dimensions.contentSpacing.y
}
Button { action: cancelAction }
@ -177,7 +162,17 @@ TabletModalWindow {
}
}
Keys.onPressed: {
Keyboard {
id: keyboard
raised: keyboardEnabled && keyboardRaised
numeric: punctuationMode
anchors {
left: parent.left
right: parent.right
top: modalWindowItem.bottom
}
}
Keys.onPressed: {
if (!visible) {
return
}

View file

@ -0,0 +1,71 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
import Qt.labs.settings 1.0
import "../../styles-uit"
import "../../controls-uit" as HifiControlsUit
import "../../controls" as HifiControls
import ".."
Item {
id: bar
x:300
y:0
width: 300
height: 300
z: -1
signal sendToScript(var message);
signal windowClosed();
property bool shown: true
onShownChanged: {
bar.visible = shown;
}
Rectangle {
anchors.fill : parent
color: "transparent"
Flow {
id: flowMain
spacing: 10
flow: Flow.TopToBottom
layoutDirection: Flow.TopToBottom
anchors.fill: parent
anchors.margins: 4
}
}
Component.onCompleted: {
// put on bottom
x = 300;
y = 0;
width = 300;
height = 300;
}
function addButton(properties) {
var component = Qt.createComponent("button.qml");
if (component.status == Component.Ready) {
var button = component.createObject(flowMain);
// copy all properites to button
var keys = Object.keys(properties).forEach(function (key) {
button[key] = properties[key];
});
return button;
} else if( component.status == Component.Error) {
console.log("Load button errors " + component.errorString());
}
}
function urlHelper(src) {
if (src.match(/\bhttp/)) {
return src;
} else {
return "../../../" + src;
}
}
}

View file

@ -25,6 +25,7 @@ import "."
Item {
id: bar
x:0
height: 255
property bool shown: true
@ -45,10 +46,10 @@ Item {
anchors.fill: parent
}
Rectangle {
Rectangle {
id: background
anchors.fill : parent
color: "#FF000000"
color: "#FF000000"
border.color: "#FFFFFF"
anchors.bottomMargin: -1
anchors.leftMargin: -1
@ -104,13 +105,25 @@ Item {
}
}
}
}
}
function relocateAndResize(newWindowWidth, newWindowHeight) {
width = newWindowWidth;
y = newWindowHeight - height;
}
function onWindowGeometryChanged(rect) {
relocateAndResize(rect.width, rect.height);
}
Component.onCompleted: {
// put on bottom
width = Window.innerWidth;
height = 255;
y = Window.innerHeight - height;
relocateAndResize(Window.innerWidth, Window.innerHeight);
Window.geometryChanged.connect(onWindowGeometryChanged); // In devices with bars appearing at startup we should listen for this
}
Component.onDestruction: {
Window.geometryChanged.disconnect(onWindowGeometryChanged);
}
function addButton(properties) {

View file

@ -30,25 +30,31 @@ Rectangle {
property string activeView: "initialize";
property bool ownershipStatusReceived: false;
property bool balanceReceived: false;
property bool availableUpdatesReceived: false;
property string baseItemName: "";
property string itemName;
property string itemId;
property string itemHref;
property string itemAuthor;
property int itemEdition: -1;
property string certificateId;
property double balanceAfterPurchase;
property bool alreadyOwned: false;
property int itemPrice: -1;
property bool isCertified;
property string itemType;
property var itemTypesArray: ["entity", "wearable", "contentSet", "app", "avatar"];
property var itemTypesText: ["entity", "wearable", "content set", "app", "avatar"];
property var buttonTextNormal: ["REZ", "WEAR", "REPLACE CONTENT SET", "INSTALL", "WEAR"];
property var buttonTextClicked: ["REZZED!", "WORN!", "CONTENT SET REPLACED!", "INSTALLED!", "AVATAR CHANGED!"]
property var buttonGlyph: [hifi.glyphs.wand, hifi.glyphs.hat, hifi.glyphs.globe, hifi.glyphs.install, hifi.glyphs.avatar];
property var itemTypesArray: ["entity", "wearable", "contentSet", "app", "avatar", "unknown"];
property var itemTypesText: ["entity", "wearable", "content set", "app", "avatar", "item"];
property var buttonTextNormal: ["REZ", "WEAR", "REPLACE CONTENT SET", "INSTALL", "WEAR", "REZ"];
property var buttonTextClicked: ["REZZED!", "WORN!", "CONTENT SET REPLACED!", "INSTALLED!", "AVATAR CHANGED!", "REZZED!"]
property var buttonGlyph: [hifi.glyphs.wand, hifi.glyphs.hat, hifi.glyphs.globe, hifi.glyphs.install, hifi.glyphs.avatar, hifi.glyphs.wand];
property bool shouldBuyWithControlledFailure: false;
property bool debugCheckoutSuccess: false;
property bool canRezCertifiedItems: Entities.canRezCertified() || Entities.canRezTmpCertified();
property string referrer;
property bool isInstalled;
property bool isUpdating;
property string baseAppURL;
// Style
color: hifi.colors.white;
Connections {
@ -103,8 +109,8 @@ Rectangle {
if (result.status !== 'success') {
console.log("Failed to get balance", result.data.message);
} else {
root.balanceReceived = true;
root.balanceAfterPurchase = result.data.balance - root.itemPrice;
root.balanceReceived = true;
root.refreshBuyUI();
}
}
@ -113,13 +119,13 @@ Rectangle {
if (result.status !== 'success') {
console.log("Failed to get Already Owned status", result.data.message);
} else {
root.ownershipStatusReceived = true;
if (result.data.marketplace_item_id === root.itemId) {
root.alreadyOwned = result.data.already_owned;
} else {
console.log("WARNING - Received 'Already Owned' status about different Marketplace ID!");
root.alreadyOwned = false;
}
root.ownershipStatusReceived = true;
root.refreshBuyUI();
}
}
@ -129,11 +135,53 @@ Rectangle {
root.isInstalled = true;
}
}
onAvailableUpdatesResult: {
if (result.status !== 'success') {
console.log("Failed to get Available Updates", result.data.message);
} else {
for (var i = 0; i < result.data.updates.length; i++) {
// If the ItemID of the item we're looking at matches EITHER the ID of a "base" item
// OR the ID of an "updated" item, we're updating.
if (root.itemId === result.data.updates[i].item_id ||
root.itemId === result.data.updates[i].updated_item_id) {
if (root.itemEdition !== -1 && root.itemEdition !== parseInt(result.data.updates[i].edition_number)) {
continue;
}
root.isUpdating = true;
root.baseItemName = result.data.updates[i].base_item_title;
// This CertID is the one corresponding to the base item CertID that the user already owns
root.certificateId = result.data.updates[i].certificate_id;
if (root.itemType === "app") {
root.baseAppURL = result.data.updates[i].item_download_url;
}
break;
}
}
root.availableUpdatesReceived = true;
refreshBuyUI();
}
}
onUpdateItemResult: {
if (result.status !== 'success') {
failureErrorText.text = result.message;
root.activeView = "checkoutFailure";
} else {
root.itemHref = result.data.download_url;
if (result.data.categories.indexOf("Wearables") > -1) {
root.itemType = "wearable";
}
root.activeView = "checkoutSuccess";
}
}
}
onItemIdChanged: {
root.ownershipStatusReceived = false;
Commerce.alreadyOwned(root.itemId);
root.availableUpdatesReceived = false;
Commerce.getAvailableUpdates(root.itemId);
itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg";
}
@ -161,6 +209,7 @@ Rectangle {
}
onItemPriceChanged: {
root.balanceReceived = false;
Commerce.balance();
}
@ -240,6 +289,7 @@ Rectangle {
Component.onCompleted: {
ownershipStatusReceived = false;
balanceReceived = false;
availableUpdatesReceived = false;
Commerce.getWalletStatus();
}
}
@ -316,7 +366,7 @@ Rectangle {
Rectangle {
id: loading;
z: 997;
visible: !root.ownershipStatusReceived || !root.balanceReceived;
visible: !root.ownershipStatusReceived || !root.balanceReceived || !root.availableUpdatesReceived;
anchors.fill: parent;
color: hifi.colors.white;
@ -412,6 +462,7 @@ Rectangle {
// "HFC" balance label
HiFiGlyphs {
id: itemPriceTextLabel;
visible: !(root.isUpdating && root.itemEdition > 0);
text: hifi.glyphs.hfc;
// Size
size: 30;
@ -427,9 +478,9 @@ Rectangle {
}
FiraSansSemiBold {
id: itemPriceText;
text: (root.itemPrice === -1) ? "--" : root.itemPrice;
text: (root.isUpdating && root.itemEdition > 0) ? "FREE\nUPDATE" : ((root.itemPrice === -1) ? "--" : root.itemPrice);
// Text size
size: 26;
size: (root.isUpdating && root.itemEdition > 0) ? 20 : 26;
// Anchors
anchors.top: parent.top;
anchors.right: parent.right;
@ -529,9 +580,13 @@ Rectangle {
height: 50;
anchors.left: parent.left;
anchors.right: parent.right;
text: "VIEW THIS ITEM IN MY PURCHASES";
text: root.isUpdating ? "UPDATE TO THIS ITEM FOR FREE" : "VIEW THIS ITEM IN MY PURCHASES";
onClicked: {
sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName});
if (root.isUpdating) {
sendToScript({method: 'checkout_goToPurchases', filterText: root.baseItemName});
} else {
sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName});
}
}
}
@ -539,7 +594,7 @@ Rectangle {
HifiControlsUit.Button {
id: buyButton;
visible: !((root.itemType === "avatar" || root.itemType === "app") && viewInMyPurchasesButton.visible)
enabled: (root.balanceAfterPurchase >= 0 && ownershipStatusReceived && balanceReceived) || (!root.isCertified);
enabled: (root.balanceAfterPurchase >= 0 && ownershipStatusReceived && balanceReceived && availableUpdatesReceived) || (!root.isCertified) || root.isUpdating;
color: viewInMyPurchasesButton.visible ? hifi.buttons.white : hifi.buttons.blue;
colorScheme: hifi.colorSchemes.light;
anchors.top: viewInMyPurchasesButton.visible ? viewInMyPurchasesButton.bottom :
@ -548,10 +603,19 @@ Rectangle {
height: 50;
anchors.left: parent.left;
anchors.right: parent.right;
text: ((root.isCertified) ? ((ownershipStatusReceived && balanceReceived) ?
(viewInMyPurchasesButton.visible ? "Buy It Again" : "Confirm Purchase") : "--") : "Get Item");
text: (root.isUpdating && root.itemEdition > 0) ? "CONFIRM UPDATE" : (((root.isCertified) ? ((ownershipStatusReceived && balanceReceived && availableUpdatesReceived) ?
((viewInMyPurchasesButton.visible && !root.isUpdating) ? "Buy It Again" : "Confirm Purchase") : "--") : "Get Item"));
onClicked: {
if (root.isCertified) {
if (root.isUpdating && root.itemEdition > 0) {
// If we're updating an app, the existing app needs to be uninstalled.
// This call will fail/return `false` if the app isn't installed, but that's OK.
if (root.itemType === "app") {
Commerce.uninstallApp(root.baseAppURL);
}
buyButton.enabled = false;
loading.visible = true;
Commerce.updateItem(root.certificateId);
} else if (root.isCertified) {
if (!root.shouldBuyWithControlledFailure) {
if (root.itemType === "contentSet" && !Entities.canReplaceContent()) {
lightboxPopup.titleText = "Purchase Content Set";
@ -975,7 +1039,7 @@ Rectangle {
buyButton.color = hifi.buttons.red;
root.shouldBuyWithControlledFailure = true;
} else {
buyButton.text = (root.isCertified ? ((ownershipStatusReceived && balanceReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item");
buyButton.text = (root.isCertified ? ((ownershipStatusReceived && balanceReceived && availableUpdatesReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item");
buyButton.color = hifi.buttons.blue;
root.shouldBuyWithControlledFailure = false;
}
@ -1001,12 +1065,13 @@ Rectangle {
function fromScript(message) {
switch (message.method) {
case 'updateCheckoutQML':
itemId = message.params.itemId;
itemName = message.params.itemName;
root.itemId = message.params.itemId;
root.itemName = message.params.itemName.trim();
root.itemPrice = message.params.itemPrice;
itemHref = message.params.itemHref;
referrer = message.params.referrer;
itemAuthor = message.params.itemAuthor;
root.itemHref = message.params.itemHref;
root.referrer = message.params.referrer;
root.itemAuthor = message.params.itemAuthor;
root.itemEdition = message.params.itemEdition || -1;
refreshBuyUI();
break;
default:
@ -1015,35 +1080,70 @@ Rectangle {
}
signal sendToScript(var message);
function canBuyAgain() {
return (root.itemType === "entity" || root.itemType === "wearable" || root.itemType === "contentSet" || root.itemType === "unknown");
}
function handleContentSets() {
if (root.itemType === "contentSet" && !Entities.canReplaceContent()) {
buyText.text = "The domain owner must enable 'Replace Content' permissions for you in this " +
"<b>domain's server settings</b> before you can replace this domain's content with <b>" + root.itemName + "</b>";
buyTextContainer.color = "#FFC3CD";
buyTextContainer.border.color = "#F3808F";
buyGlyph.text = hifi.glyphs.alert;
buyGlyph.size = 54;
}
}
function handleBuyAgainLogic() {
// If you can buy this item again...
if (canBuyAgain()) {
// If you can't afford another copy of the item...
if (root.balanceAfterPurchase < 0) {
// If you already own the item...
if (root.alreadyOwned) {
buyText.text = "<b>Your Wallet does not have sufficient funds to purchase this item again.</b>";
// Else if you don't already own the item...
} else {
buyText.text = "<b>Your Wallet does not have sufficient funds to purchase this item.</b>";
}
buyTextContainer.color = "#FFC3CD";
buyTextContainer.border.color = "#F3808F";
buyGlyph.text = hifi.glyphs.alert;
buyGlyph.size = 54;
// If you CAN afford another copy of the item...
} else {
handleContentSets();
}
}
}
function refreshBuyUI() {
if (root.isCertified) {
if (root.ownershipStatusReceived && root.balanceReceived) {
if (root.balanceAfterPurchase < 0) {
if (root.alreadyOwned) {
buyText.text = "<b>Your Wallet does not have sufficient funds to purchase this item again.</b>";
viewInMyPurchasesButton.visible = true;
if (root.ownershipStatusReceived && root.balanceReceived && root.availableUpdatesReceived) {
buyText.text = "";
// If the user IS on the checkout page for the updated version of an owned item...
if (root.isUpdating) {
// If the user HAS already selected a specific edition to update...
if (root.itemEdition > 0) {
buyText.text = "By pressing \"Confirm Update\", you agree to trade in your old item for the updated item that replaces it.";
buyTextContainer.color = "#FFFFFF";
buyTextContainer.border.color = "#FFFFFF";
// Else if the user HAS NOT selected a specific edition to update...
} else {
buyText.text = "<b>Your Wallet does not have sufficient funds to purchase this item.</b>";
}
buyTextContainer.color = "#FFC3CD";
buyTextContainer.border.color = "#F3808F";
buyGlyph.text = hifi.glyphs.alert;
buyGlyph.size = 54;
viewInMyPurchasesButton.visible = true;
handleBuyAgainLogic();
}
// If the user IS NOT on the checkout page for the updated verison of an owned item...
// (i.e. they are checking out an item "normally")
} else {
if (root.alreadyOwned) {
viewInMyPurchasesButton.visible = true;
} else {
buyText.text = "";
}
if (root.itemType === "contentSet" && !Entities.canReplaceContent()) {
buyText.text = "The domain owner must enable 'Replace Content' permissions for you in this " +
"<b>domain's server settings</b> before you can replace this domain's content with <b>" + root.itemName + "</b>";
buyTextContainer.color = "#FFC3CD";
buyTextContainer.border.color = "#F3808F";
buyGlyph.text = hifi.glyphs.alert;
buyGlyph.size = 54;
}
handleBuyAgainLogic();
}
} else {
buyText.text = "";
@ -1062,11 +1162,13 @@ Rectangle {
root.activeView = "checkoutMain";
} else {
root.activeView = "checkoutSuccess";
root.ownershipStatusReceived = false;
Commerce.alreadyOwned(root.itemId);
root.availableUpdatesReceived = false;
Commerce.getAvailableUpdates(root.itemId);
root.balanceReceived = false;
Commerce.balance();
}
root.balanceReceived = false;
root.ownershipStatusReceived = false;
Commerce.alreadyOwned(root.itemId);
Commerce.balance();
}
//

View file

@ -28,6 +28,7 @@ Item {
property string referrerURL: (Account.metaverseServerURL + "/marketplace?");
readonly property int additionalDropdownHeight: usernameDropdown.height - myUsernameButton.anchors.bottomMargin;
property alias usernameDropdownVisible: usernameDropdown.visible;
property bool messagesWaiting: false;
height: mainContainer.height + additionalDropdownHeight;
@ -38,6 +39,7 @@ Item {
if (walletStatus === 0) {
sendToParent({method: "needsLogIn"});
} else if (walletStatus === 5) {
Commerce.getAvailableUpdates();
Commerce.getSecurityImage();
} else if (walletStatus > 5) {
console.log("ERROR in EmulatedMarketplaceHeader.qml: Unknown wallet status: " + walletStatus);
@ -58,6 +60,14 @@ Item {
securityImage.source = "image://security/securityImage";
}
}
onAvailableUpdatesResult: {
if (result.status !== 'success') {
console.log("Failed to get Available Updates", result.data.message);
} else {
root.messagesWaiting = result.data.updates.length > 0;
}
}
}
Component.onCompleted: {
@ -134,13 +144,25 @@ Item {
anchors.fill: parent;
hoverEnabled: enabled;
onClicked: {
sendToParent({method: 'header_goToPurchases'});
sendToParent({ method: 'header_goToPurchases', hasUpdates: root.messagesWaiting });
}
onEntered: myPurchasesText.color = hifi.colors.blueHighlight;
onExited: myPurchasesText.color = hifi.colors.blueAccent;
}
}
Rectangle {
id: messagesWaitingLight;
visible: root.messagesWaiting;
anchors.right: myPurchasesLink.left;
anchors.rightMargin: -2;
anchors.verticalCenter: parent.verticalCenter;
height: 10;
width: height;
radius: height/2;
color: "red";
}
TextMetrics {
id: textMetrics;
font.family: "Raleway"

View file

@ -48,11 +48,14 @@ Item {
property bool hasPermissionToRezThis;
property bool permissionExplanationCardVisible;
property bool isInstalled;
property string upgradeUrl;
property string upgradeTitle;
property bool isShowingMyItems;
property string originalStatusText;
property string originalStatusColor;
height: 110;
height: (root.upgradeUrl === "" || root.isShowingMyItems) ? 110 : 150;
width: parent.width;
Connections {
@ -137,6 +140,14 @@ Item {
anchors.verticalCenter: parent.verticalCenter;
height: root.height - 10;
// START "incorrect indentation to prevent insane diffs"
Item {
id: itemContainer;
anchors.left: parent.left;
anchors.right: parent.right;
anchors.top: parent.top;
height: 100;
Image {
id: itemPreviewImage;
source: root.itemPreviewImageUrl;
@ -357,7 +368,7 @@ Item {
Item {
id: statusContainer;
visible: root.purchaseStatus === "pending" || root.purchaseStatus === "invalidated" || root.purchaseStatusChanged;
visible: root.purchaseStatus === "pending" || root.purchaseStatus === "invalidated" || root.purchaseStatusChanged || root.numberSold > -1;
anchors.left: itemName.left;
anchors.top: certificateContainer.bottom;
anchors.topMargin: 8;
@ -376,7 +387,7 @@ Item {
"PENDING..."
} else if (root.purchaseStatus === "invalidated") {
"INVALIDATED"
} else if (root.numberSold !== -1) {
} else if (root.numberSold > -1) {
("Sales: " + root.numberSold + "/" + (root.limitedRun === -1 ? "\u221e" : root.limitedRun))
} else {
""
@ -634,6 +645,48 @@ Item {
}
}
}
}
// END "incorrect indentation to prevent insane diffs"
Rectangle {
id: upgradeAvailableContainer;
visible: root.upgradeUrl !== "" && !root.isShowingMyItems;
anchors.top: itemContainer.bottom;
anchors.bottom: parent.bottom;
anchors.left: parent.left;
anchors.right: parent.right;
color: "#B5EAFF";
RalewayRegular {
id: updateAvailableText;
text: "UPDATE AVAILABLE";
size: 13;
anchors.left: parent.left;
anchors.leftMargin: 12;
anchors.top: parent.top;
anchors.bottom: parent.bottom;
width: paintedWidth;
color: hifi.colors.black;
verticalAlignment: Text.AlignVCenter;
}
RalewaySemiBold {
id: updateNowText;
text: "<font color='#0093C5'><a href='#'>Update this item now</a></font>";
size: 13;
anchors.left: updateAvailableText.right;
anchors.leftMargin: 16;
anchors.top: parent.top;
anchors.bottom: parent.bottom;
width: paintedWidth;
color: hifi.colors.black;
verticalAlignment: Text.AlignVCenter;
onLinkActivated: {
sendToPurchases({method: 'updateItemClicked', itemId: root.itemId, itemEdition: root.itemEdition, upgradeUrl: root.upgradeUrl});
}
}
}
}
DropShadow {

View file

@ -37,6 +37,8 @@ Rectangle {
property bool isDebuggingFirstUseTutorial: false;
property int pendingItemCount: 0;
property string installedApps;
property bool keyboardRaised: false;
property int numUpdatesAvailable: 0;
// Style
color: hifi.colors.white;
Connections {
@ -64,6 +66,7 @@ Rectangle {
root.activeView = "purchasesMain";
root.installedApps = Commerce.getInstalledApps();
Commerce.inventory();
Commerce.getAvailableUpdates();
}
} else {
console.log("ERROR in Purchases.qml: Unknown wallet status: " + walletStatus);
@ -119,6 +122,15 @@ Rectangle {
root.pendingInventoryReply = false;
}
onAvailableUpdatesResult: {
if (result.status !== 'success') {
console.log("Failed to get Available Updates", result.data.message);
} else {
sendToScript({method: 'purchases_availableUpdatesReceived', numUpdates: result.data.updates.length });
root.numUpdatesAvailable = result.data.updates.length;
}
}
}
Timer {
@ -273,6 +285,7 @@ Rectangle {
root.activeView = "purchasesMain";
root.installedApps = Commerce.getInstalledApps();
Commerce.inventory();
Commerce.getAvailableUpdates();
break;
}
}
@ -296,6 +309,7 @@ Rectangle {
// FILTER BAR START
//
Item {
z: 997;
id: filterBarContainer;
// Size
height: 40;
@ -321,28 +335,61 @@ Rectangle {
size: 22;
}
HifiControlsUit.TextField {
HifiControlsUit.FilterBar {
id: filterBar;
property string previousText: "";
property string previousPrimaryFilter: "";
colorScheme: hifi.colorSchemes.faintGray;
hasClearButton: true;
hasRoundedBorder: true;
anchors.top: parent.top;
anchors.right: parent.right;
anchors.left: myText.right;
anchors.leftMargin: 16;
height: 39;
anchors.verticalCenter: parent.verticalCenter;
anchors.right: parent.right;
textFieldHeight: 39;
height: textFieldHeight + dropdownHeight;
placeholderText: "filter items";
Component.onCompleted: {
var choices = [
{
"displayName": "App",
"filterName": "app"
},
{
"displayName": "Avatar",
"filterName": "avatar"
},
{
"displayName": "Content Set",
"filterName": "contentSet"
},
{
"displayName": "Entity",
"filterName": "entity"
},
{
"displayName": "Wearable",
"filterName": "wearable"
},
{
"displayName": "Updatable",
"filterName": "updatable"
}
]
filterBar.primaryFilterChoices.clear();
filterBar.primaryFilterChoices.append(choices);
}
onPrimaryFilter_displayNameChanged: {
buildFilteredPurchasesModel();
purchasesContentsList.positionViewAtIndex(0, ListView.Beginning)
filterBar.previousPrimaryFilter = filterBar.primaryFilter_displayName;
}
onTextChanged: {
buildFilteredPurchasesModel();
purchasesContentsList.positionViewAtIndex(0, ListView.Beginning)
filterBar.previousText = filterBar.text;
}
onAccepted: {
focus = false;
}
}
}
//
@ -350,6 +397,7 @@ Rectangle {
//
HifiControlsUit.Separator {
z: 996;
id: separator;
colorScheme: 2;
anchors.left: parent.left;
@ -377,12 +425,11 @@ Rectangle {
clip: true;
model: filteredPurchasesModel;
snapMode: ListView.SnapToItem;
highlightRangeMode: ListView.StrictlyEnforceRange;
// Anchors
anchors.top: separator.bottom;
anchors.topMargin: 12;
anchors.left: parent.left;
anchors.bottom: parent.bottom;
anchors.bottom: updatesAvailableBanner.visible ? updatesAvailableBanner.top : parent.bottom;
width: parent.width;
delegate: PurchasedItem {
itemName: title;
@ -398,21 +445,10 @@ Rectangle {
displayedItemCount: model.displayedItemCount;
permissionExplanationCardVisible: model.permissionExplanationCardVisible;
isInstalled: model.isInstalled;
itemType: {
if (model.root_file_url.indexOf(".fst") > -1) {
"avatar";
} else if (model.categories.indexOf("Wearables") > -1) {
"wearable";
} else if (model.root_file_url.endsWith('.json.gz')) {
"contentSet";
} else if (model.root_file_url.endsWith('.app.json')) {
"app";
} else if (model.root_file_url.endsWith('.json')) {
"entity";
} else {
"unknown";
}
}
upgradeUrl: model.upgrade_url;
upgradeTitle: model.upgrade_title;
itemType: model.itemType;
isShowingMyItems: root.isShowingMyItems;
anchors.topMargin: 10;
anchors.bottomMargin: 10;
@ -485,15 +521,80 @@ Rectangle {
filteredPurchasesModel.setProperty(i, "permissionExplanationCardVisible", true);
}
}
} else if (msg.method === "updateItemClicked") {
sendToScript(msg);
}
}
}
}
}
Rectangle {
id: updatesAvailableBanner;
visible: root.numUpdatesAvailable > 0 && !root.isShowingMyItems;
anchors.bottom: parent.bottom;
anchors.left: parent.left;
anchors.right: parent.right;
height: 75;
color: "#B5EAFF";
Rectangle {
id: updatesAvailableGlyph;
anchors.verticalCenter: parent.verticalCenter;
anchors.left: parent.left;
anchors.leftMargin: 16;
// Size
width: 10;
height: width;
radius: width/2;
// Style
color: "red";
}
RalewaySemiBold {
text: "You have " + root.numUpdatesAvailable + " item updates available.";
// Text size
size: 18;
// Anchors
anchors.left: updatesAvailableGlyph.right;
anchors.leftMargin: 12;
height: parent.height;
width: paintedWidth;
// Style
color: hifi.colors.black;
// Alignment
verticalAlignment: Text.AlignVCenter;
}
MouseArea {
anchors.fill: parent;
hoverEnabled: true;
propagateComposedEvents: false;
}
HifiControlsUit.Button {
color: hifi.buttons.white;
colorScheme: hifi.colorSchemes.dark;
anchors.verticalCenter: parent.verticalCenter;
anchors.right: parent.right;
anchors.rightMargin: 12;
width: 100;
height: 40;
text: "SHOW ME";
onClicked: {
filterBar.text = "";
filterBar.changeFilterByDisplayName("Updatable");
}
}
}
Item {
id: noItemsAlertContainer;
visible: !purchasesContentsList.visible && root.purchasesReceived && root.isShowingMyItems && filterBar.text === "";
visible: !purchasesContentsList.visible &&
root.purchasesReceived &&
root.isShowingMyItems &&
filterBar.text === "" &&
filterBar.primaryFilter_displayName === "";
anchors.top: filterBarContainer.bottom;
anchors.topMargin: 12;
anchors.left: parent.left;
@ -539,7 +640,11 @@ Rectangle {
Item {
id: noPurchasesAlertContainer;
visible: !purchasesContentsList.visible && root.purchasesReceived && !root.isShowingMyItems && filterBar.text === "";
visible: !purchasesContentsList.visible &&
root.purchasesReceived &&
!root.isShowingMyItems &&
filterBar.text === "" &&
filterBar.primaryFilter_displayName === "";
anchors.top: filterBarContainer.bottom;
anchors.topMargin: 12;
anchors.left: parent.left;
@ -589,7 +694,7 @@ Rectangle {
HifiControlsUit.Keyboard {
id: keyboard;
raised: HMD.mounted && filterBar.focus;
raised: HMD.mounted && parent.keyboardRaised;
numeric: parent.punctuationMode;
anchors {
bottom: parent.bottom;
@ -613,6 +718,7 @@ Rectangle {
console.log("Refreshing Purchases...");
root.pendingInventoryReply = true;
Commerce.inventory();
Commerce.getAvailableUpdates();
}
}
}
@ -660,8 +766,13 @@ Rectangle {
var sameItemCount = 0;
tempPurchasesModel.clear();
for (var i = 0; i < purchasesModel.count; i++) {
if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) {
if (!purchasesModel.get(i).valid) {
continue;
}
if (purchasesModel.get(i).status !== "confirmed" && !root.isShowingMyItems) {
tempPurchasesModel.insert(0, purchasesModel.get(i));
} else if ((root.isShowingMyItems && purchasesModel.get(i).edition_number === "0") ||
@ -671,6 +782,35 @@ Rectangle {
}
}
// primaryFilter filtering and adding of itemType property to model
var currentItemType, currentRootFileUrl, currentCategories;
for (var i = 0; i < tempPurchasesModel.count; i++) {
currentRootFileUrl = tempPurchasesModel.get(i).root_file_url;
currentCategories = tempPurchasesModel.get(i).categories;
if (currentRootFileUrl.indexOf(".fst") > -1) {
currentItemType = "avatar";
} else if (currentCategories.indexOf("Wearables") > -1) {
currentItemType = "wearable";
} else if (currentRootFileUrl.endsWith('.json.gz')) {
currentItemType = "contentSet";
} else if (currentRootFileUrl.endsWith('.app.json')) {
currentItemType = "app";
} else if (currentRootFileUrl.endsWith('.json')) {
currentItemType = "entity";
} else {
currentItemType = "unknown";
}
if (filterBar.primaryFilter_displayName !== "" &&
((filterBar.primaryFilter_displayName === "Updatable" && tempPurchasesModel.get(i).upgrade_url === "") ||
(filterBar.primaryFilter_displayName !== "Updatable" && filterBar.primaryFilter_filterName.toLowerCase() !== currentItemType.toLowerCase()))) {
tempPurchasesModel.remove(i);
i--;
} else {
tempPurchasesModel.setProperty(i, 'itemType', currentItemType);
}
}
for (var i = 0; i < tempPurchasesModel.count; i++) {
if (!filteredPurchasesModel.get(i)) {
sameItemCount = -1;
@ -682,12 +822,17 @@ Rectangle {
}
}
if (sameItemCount !== tempPurchasesModel.count || filterBar.text !== filterBar.previousText) {
if (sameItemCount !== tempPurchasesModel.count ||
filterBar.text !== filterBar.previousText ||
filterBar.primaryFilter !== filterBar.previousPrimaryFilter) {
filteredPurchasesModel.clear();
var currentId;
for (var i = 0; i < tempPurchasesModel.count; i++) {
currentId = tempPurchasesModel.get(i).id;
if (!purchasesModel.get(i).valid) {
continue;
}
filteredPurchasesModel.append(tempPurchasesModel.get(i));
filteredPurchasesModel.setProperty(i, 'permissionExplanationCardVisible', false);
filteredPurchasesModel.setProperty(i, 'isInstalled', ((root.installedApps).indexOf(currentId) > -1));
@ -736,7 +881,7 @@ Rectangle {
function fromScript(message) {
switch (message.method) {
case 'updatePurchases':
referrerURL = message.referrerURL;
referrerURL = message.referrerURL || "";
titleBarContainer.referrerURL = message.referrerURL;
filterBar.text = message.filterText ? message.filterText : "";
break;

View file

@ -39,6 +39,7 @@ Item {
root.noMoreHistoryData = false;
root.historyRequestPending = true;
Commerce.history(root.currentHistoryPage);
Commerce.getAvailableUpdates();
} else {
refreshTimer.stop();
}
@ -133,6 +134,14 @@ Item {
refreshTimer.start();
}
}
onAvailableUpdatesResult: {
if (result.status !== 'success') {
console.log("Failed to get Available Updates", result.data.message);
} else {
sendToScript({method: 'wallet_availableUpdatesReceived', numUpdates: result.data.updates.length });
}
}
}
Connections {

View file

@ -377,8 +377,6 @@ static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop";
static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system";
static const QString DOMAIN_SPAWNING_POINT = "/0, -10, 0";
const std::vector<std::pair<QString, Application::AcceptURLMethod>> Application::_acceptedExtensions {
{ SVO_EXTENSION, &Application::importSVOFromURL },
{ SVO_JSON_EXTENSION, &Application::importSVOFromURL },
@ -513,6 +511,27 @@ std::atomic<uint64_t> DeadlockWatchdogThread::_maxElapsed;
std::atomic<int> DeadlockWatchdogThread::_maxElapsedAverage;
ThreadSafeMovingAverage<int, DeadlockWatchdogThread::HEARTBEAT_SAMPLES> DeadlockWatchdogThread::_movingAverage;
bool isDomainURL(QUrl url) {
if (!url.isValid()) {
return false;
}
if (url.scheme() == URL_SCHEME_HIFI) {
return true;
}
if (url.scheme() != URL_SCHEME_FILE) {
// TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can
// be loaded over http(s)
// && url.scheme() != URL_SCHEME_HTTP &&
// url.scheme() != URL_SCHEME_HTTPS
return false;
}
if (url.path().endsWith(".json", Qt::CaseInsensitive) ||
url.path().endsWith(".json.gz", Qt::CaseInsensitive)) {
return true;
}
return false;
}
#ifdef Q_OS_WIN
class MyNativeEventFilter : public QAbstractNativeEventFilter {
public:
@ -542,7 +561,7 @@ public:
if (message->message == WM_COPYDATA) {
COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)(message->lParam);
QUrl url = QUrl((const char*)(pcds->lpData));
if (url.isValid() && url.scheme() == HIFI_URL_SCHEME) {
if (isDomainURL(url)) {
DependencyManager::get<AddressManager>()->handleLookupString(url.toString());
return true;
}
@ -940,6 +959,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
setProperty(hifi::properties::STEAM, (steamClient && steamClient->isRunning()));
setProperty(hifi::properties::CRASHED, _previousSessionCrashed);
_entityClipboard->setIsServerlessMode(true);
{
const QString TEST_SCRIPT = "--testScript";
const QString TRACE_FILE = "--traceFile";
@ -1035,7 +1056,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// setup a timer for domain-server check ins
QTimer* domainCheckInTimer = new QTimer(this);
connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn);
connect(domainCheckInTimer, &QTimer::timeout, [this, nodeList] {
if (!isServerlessMode()) {
nodeList->sendDomainServerCheckIn();
}
});
domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS);
connect(this, &QCoreApplication::aboutToQuit, [domainCheckInTimer] {
domainCheckInTimer->stop();
@ -1097,9 +1122,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
const DomainHandler& domainHandler = nodeList->getDomainHandler();
connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&)));
connect(&domainHandler, SIGNAL(domainURLChanged(QUrl)), SLOT(domainURLChanged(QUrl)));
connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain()));
connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &Application::clearDomainAvatars);
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() {
@ -2046,7 +2071,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout);
connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose);
connect(&domainHandler, &DomainHandler::hostnameChanged, this, &Application::addAssetToWorldMessageClose);
connect(&domainHandler, &DomainHandler::domainURLChanged, this, &Application::addAssetToWorldMessageClose);
updateSystemTabletMode();
@ -3023,8 +3048,8 @@ void Application::handleSandboxStatus(QNetworkReply* reply) {
QString sentTo;
// If this is a first run we short-circuit the address passed in
if (firstRun.get()) {
// If this is a first run we short-circuit the address passed in
if (firstRun.get()) {
#if defined(Q_OS_ANDROID)
qCDebug(interfaceapp) << "First run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("default location") : addressLookupString);
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
@ -3042,11 +3067,11 @@ void Application::handleSandboxStatus(QNetworkReply* reply) {
#endif
firstRun.set(false);
} else {
qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString);
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
sentTo = SENT_TO_PREVIOUS_LOCATION;
}
} else {
qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString);
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
sentTo = SENT_TO_PREVIOUS_LOCATION;
}
UserActivityLogger::getInstance().logAction("startup_sent_to", {
{ "sent_to", sentTo },
@ -3086,6 +3111,57 @@ bool Application::importFromZIP(const QString& filePath) {
return true;
}
bool Application::isServerlessMode() const {
auto tree = getEntities()->getTree();
if (tree) {
return tree->isServerlessMode();
}
return false;
}
void Application::setIsServerlessMode(bool serverlessDomain) {
auto tree = getEntities()->getTree();
if (tree) {
tree->setIsServerlessMode(serverlessDomain);
}
}
void Application::loadServerlessDomain(QUrl domainURL) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_ARG(QUrl, domainURL));
return;
}
if (domainURL.isEmpty()) {
return;
}
QUuid serverlessSessionID = QUuid::createUuid();
getMyAvatar()->setSessionUUID(serverlessSessionID);
auto nodeList = DependencyManager::get<NodeList>();
nodeList->setSessionUUID(serverlessSessionID);
// there is no domain-server to tell us our permissions, so enable all
NodePermissions permissions;
permissions.setAll(true);
nodeList->setPermissions(permissions);
// we can't import directly into the main tree because we would need to lock it, and
// Octree::readFromURL calls loop.exec which can run code which will also attempt to lock the tree.
EntityTreePointer tmpTree(new EntityTree());
tmpTree->setIsServerlessMode(true);
tmpTree->createRootElement();
auto myAvatar = getMyAvatar();
tmpTree->setMyAvatar(myAvatar);
bool success = tmpTree->readFromURL(domainURL.toString());
if (success) {
tmpTree->reaverageOctreeElements();
tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), 0, 0, 0);
}
_fullSceneReceivedCounter++;
}
bool Application::importImage(const QString& urlString) {
qCDebug(interfaceapp) << "An image file has been dropped in";
QString filepath(urlString);
@ -4586,7 +4662,7 @@ void Application::initDisplay() {
}
void Application::init() {
// Make sure Login state is up to date
DependencyManager::get<DialogsManager>()->toggleLoginDialog();
if (!DISABLE_DEFERRED) {
@ -4611,7 +4687,9 @@ void Application::init() {
qCDebug(interfaceapp) << "Loaded settings";
// fire off an immediate domain-server check in now that settings are loaded
DependencyManager::get<NodeList>()->sendDomainServerCheckIn();
if (!isServerlessMode()) {
DependencyManager::get<NodeList>()->sendDomainServerCheckIn();
}
// This allows collision to be set up properly for shape entities supported by GeometryCache.
// This is before entity setup to ensure that it's ready for whenever instance collision is initialized.
@ -5745,10 +5823,15 @@ void Application::updateWindowTitle() const {
QString connectionStatus = nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED)";
QString username = accountManager->getAccountInfo().getUsername();
QString currentPlaceName = DependencyManager::get<AddressManager>()->getHost();
if (currentPlaceName.isEmpty()) {
currentPlaceName = nodeList->getDomainHandler().getHostname();
QString currentPlaceName;
if (isServerlessMode()) {
currentPlaceName = "serverless: " + DependencyManager::get<AddressManager>()->getDomainURL().toString();
} else {
currentPlaceName = DependencyManager::get<AddressManager>()->getDomainURL().host();
if (currentPlaceName.isEmpty()) {
currentPlaceName = nodeList->getDomainHandler().getHostname();
}
}
QString title = QString() + (!username.isEmpty() ? username + " @ " : QString())
@ -5761,7 +5844,7 @@ void Application::updateWindowTitle() const {
_window->setWindowTitle(title);
// updateTitleWindow gets called whenever there's a change regarding the domain, so rather
// than placing this within domainChanged, it's placed here to cover the other potential cases.
// than placing this within domainURLChanged, it's placed here to cover the other potential cases.
DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", "");
}
@ -5800,15 +5883,22 @@ void Application::clearDomainAvatars() {
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
}
void Application::domainChanged(const QString& domainHostname) {
updateWindowTitle();
void Application::domainURLChanged(QUrl domainURL) {
// disable physics until we have enough information about our new location to not cause craziness.
resetPhysicsReadyInformation();
setIsServerlessMode(domainURL.scheme() != URL_SCHEME_HIFI);
if (isServerlessMode()) {
loadServerlessDomain(domainURL);
}
updateWindowTitle();
}
void Application::resettingDomain() {
_notifiedPacketVersionMismatchThisDomain = false;
auto nodeList = DependencyManager::get<NodeList>();
clearDomainOctreeDetails();
}
void Application::nodeAdded(SharedNodePointer node) const {
@ -5925,22 +6015,22 @@ bool Application::nearbyEntitiesAreReadyForPhysics() {
AABox avatarBox(getMyAvatar()->getWorldPosition() - glm::vec3(PHYSICS_READY_RANGE), glm::vec3(2 * PHYSICS_READY_RANGE));
// create two functions that use avatarBox (entityScan and elementScan), the second calls the first
std::function<bool (EntityItemPointer&)> entityScan = [=](EntityItemPointer& entity) {
if (entity->shouldBePhysical()) {
bool success = false;
AABox entityBox = entity->getAABox(success);
// important: bail for entities that cannot supply a valid AABox
return success && avatarBox.touches(entityBox);
}
return false;
};
if (entity->shouldBePhysical()) {
bool success = false;
AABox entityBox = entity->getAABox(success);
// important: bail for entities that cannot supply a valid AABox
return success && avatarBox.touches(entityBox);
}
return false;
};
std::function<bool(const OctreeElementPointer&, void*)> elementScan = [&](const OctreeElementPointer& element, void* unused) {
if (element->getAACube().touches(avatarBox)) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->getEntities(entityScan, entities);
return true;
}
return false;
};
if (element->getAACube().touches(avatarBox)) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->getEntities(entityScan, entities);
return true;
}
return false;
};
entityTree->withReadLock([&] {
// Pass the second function to the general-purpose EntityTree::findEntities()
@ -6176,7 +6266,7 @@ bool Application::canAcceptURL(const QString& urlString) const {
QUrl url(urlString);
if (url.query().contains(WEB_VIEW_TAG)) {
return false;
} else if (urlString.startsWith(HIFI_URL_SCHEME)) {
} else if (urlString.startsWith(URL_SCHEME_HIFI)) {
return true;
}
QString lowerPath = url.path().toLower();
@ -6189,15 +6279,14 @@ bool Application::canAcceptURL(const QString& urlString) const {
}
bool Application::acceptURL(const QString& urlString, bool defaultUpload) {
if (urlString.startsWith(HIFI_URL_SCHEME)) {
// this is a hifi URL - have the AddressManager handle it
emit receivedHifiSchemeURL(urlString);
QUrl url(urlString);
if (isDomainURL(url)) {
// this is a URL for a domain, either hifi:// or serverless - have the AddressManager handle it
QMetaObject::invokeMethod(DependencyManager::get<AddressManager>().data(), "handleLookupString",
Qt::AutoConnection, Q_ARG(const QString&, urlString));
return true;
}
QUrl url(urlString);
QString lowerPath = url.path().toLower();
for (auto& pair : _acceptedExtensions) {
if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) {
@ -7036,7 +7125,7 @@ void Application::packageModel() {
void Application::openUrl(const QUrl& url) const {
if (!url.isEmpty()) {
if (url.scheme() == HIFI_URL_SCHEME) {
if (url.scheme() == URL_SCHEME_HIFI) {
DependencyManager::get<AddressManager>()->handleLookupString(url.toString());
} else {
// address manager did not handle - ask QDesktopServices to handle

View file

@ -284,6 +284,8 @@ public:
bool getSaveAvatarOverrideUrl() { return _saveAvatarOverrideUrl; }
void saveNextPhysicsStats(QString filename);
bool isServerlessMode() const;
void replaceDomainContent(const QString& url);
signals:
@ -295,7 +297,6 @@ signals:
void activeDisplayPluginChanged();
void uploadRequest(QString path);
void receivedHifiSchemeURL(const QString& url);
public slots:
QVector<EntityItemID> pasteEntities(float x, float y, float z);
@ -391,6 +392,9 @@ public slots:
const QString getPreferredCursor() const { return _preferredCursor.get(); }
void setPreferredCursor(const QString& cursor);
void setIsServerlessMode(bool serverlessDomain);
void loadServerlessDomain(QUrl domainURL);
Q_INVOKABLE bool askBeforeSetAvatarUrl(const QString& avatarUrl) { return askToSetAvatarUrl(avatarUrl); }
private slots:
@ -425,7 +429,7 @@ private slots:
void setSessionUUID(const QUuid& sessionUUID) const;
void domainChanged(const QString& domainHostname);
void domainURLChanged(QUrl domainURL);
void updateWindowTitle() const;
void nodeAdded(SharedNodePointer node) const;
void nodeActivated(SharedNodePointer node);

View file

@ -49,7 +49,7 @@ void DiscoverabilityManager::updateLocation() {
auto accountManager = DependencyManager::get<AccountManager>();
auto addressManager = DependencyManager::get<AddressManager>();
auto& domainHandler = DependencyManager::get<NodeList>()->getDomainHandler();
bool discoverable = (_mode.get() != Discoverability::None);
bool discoverable = (_mode.get() != Discoverability::None) && !domainHandler.isServerless();
if (accountManager->isLoggedIn()) {

View file

@ -52,6 +52,8 @@ Handler(inventory)
Handler(transferHfcToNode)
Handler(transferHfcToUsername)
Handler(alreadyOwned)
Handler(availableUpdates)
Handler(updateItem)
void Ledger::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request) {
auto accountManager = DependencyManager::get<AccountManager>();
@ -376,3 +378,23 @@ void Ledger::alreadyOwned(const QString& marketplaceId) {
qDebug(commerce) << "User attempted to use the alreadyOwned endpoint, but cachedPublicKeys was empty!";
}
}
void Ledger::getAvailableUpdates(const QString& itemId) {
auto wallet = DependencyManager::get<Wallet>();
QString endpoint = "available_updates";
QJsonObject request;
request["public_keys"] = QJsonArray::fromStringList(wallet->listPublicKeys());
if (!itemId.isEmpty()) {
request["marketplace_item_id"] = itemId;
}
send(endpoint, "availableUpdatesSuccess", "availableUpdatesFailure", QNetworkAccessManager::PutOperation, AccountManagerAuth::Required, request);
}
void Ledger::updateItem(const QString& hfc_key, const QString& certificate_id) {
QJsonObject transaction;
transaction["public_key"] = hfc_key;
transaction["certificate_id"] = certificate_id;
QJsonDocument transactionDoc{ transaction };
auto transactionString = transactionDoc.toJson(QJsonDocument::Compact);
signedSend("transaction", transactionString, hfc_key, "update_item", "updateItemSuccess", "updateItemFailure");
}

View file

@ -36,6 +36,8 @@ public:
void transferHfcToNode(const QString& hfc_key, const QString& nodeID, const int& amount, const QString& optionalMessage);
void transferHfcToUsername(const QString& hfc_key, const QString& username, const int& amount, const QString& optionalMessage);
void alreadyOwned(const QString& marketplaceId);
void getAvailableUpdates(const QString& itemId = "");
void updateItem(const QString& hfc_key, const QString& certificate_id);
enum CertificateStatus {
CERTIFICATE_STATUS_UNKNOWN = 0,
@ -57,6 +59,8 @@ signals:
void transferHfcToNodeResult(QJsonObject result);
void transferHfcToUsernameResult(QJsonObject result);
void alreadyOwnedResult(QJsonObject result);
void availableUpdatesResult(QJsonObject result);
void updateItemResult(QJsonObject result);
void updateCertificateStatus(const QString& certID, uint certStatus);
@ -83,6 +87,10 @@ public slots:
void transferHfcToUsernameFailure(QNetworkReply& reply);
void alreadyOwnedSuccess(QNetworkReply& reply);
void alreadyOwnedFailure(QNetworkReply& reply);
void availableUpdatesSuccess(QNetworkReply& reply);
void availableUpdatesFailure(QNetworkReply& reply);
void updateItemSuccess(QNetworkReply& reply);
void updateItemFailure(QNetworkReply& reply);
private:
QJsonObject apiResponse(const QString& label, QNetworkReply& reply);

View file

@ -38,7 +38,8 @@ QmlCommerce::QmlCommerce() {
connect(ledger.data(), &Ledger::updateCertificateStatus, this, &QmlCommerce::updateCertificateStatus);
connect(ledger.data(), &Ledger::transferHfcToNodeResult, this, &QmlCommerce::transferHfcToNodeResult);
connect(ledger.data(), &Ledger::transferHfcToUsernameResult, this, &QmlCommerce::transferHfcToUsernameResult);
connect(ledger.data(), &Ledger::transferHfcToUsernameResult, this, &QmlCommerce::transferHfcToUsernameResult);
connect(ledger.data(), &Ledger::availableUpdatesResult, this, &QmlCommerce::availableUpdatesResult);
connect(ledger.data(), &Ledger::updateItemResult, this, &QmlCommerce::updateItemResult);
auto accountManager = DependencyManager::get<AccountManager>();
connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() {
@ -349,3 +350,20 @@ bool QmlCommerce::openApp(const QString& itemHref) {
return true;
}
void QmlCommerce::getAvailableUpdates(const QString& itemId) {
auto ledger = DependencyManager::get<Ledger>();
ledger->getAvailableUpdates(itemId);
}
void QmlCommerce::updateItem(const QString& certificateId) {
auto ledger = DependencyManager::get<Ledger>();
auto wallet = DependencyManager::get<Wallet>();
QStringList keys = wallet->listPublicKeys();
if (keys.count() == 0) {
QJsonObject result{ { "status", "fail" },{ "message", "Uninitialized Wallet." } };
return emit updateItemResult(result);
}
QString key = keys[0];
ledger->updateItem(key, certificateId);
}

View file

@ -43,6 +43,8 @@ signals:
void accountResult(QJsonObject result);
void certificateInfoResult(QJsonObject result);
void alreadyOwnedResult(QJsonObject result);
void availableUpdatesResult(QJsonObject result);
void updateItemResult(QJsonObject result);
void updateCertificateStatus(const QString& certID, uint certStatus);
@ -89,6 +91,9 @@ protected:
Q_INVOKABLE bool uninstallApp(const QString& appHref);
Q_INVOKABLE bool openApp(const QString& appHref);
Q_INVOKABLE void getAvailableUpdates(const QString& itemId = "");
Q_INVOKABLE void updateItem(const QString& certificateId);
private:
QString _appsPath;
};

View file

@ -129,7 +129,7 @@ int main(int argc, const char* argv[]) {
if (socket.waitForConnected(LOCAL_SERVER_TIMEOUT_MS)) {
if (parser.isSet(urlOption)) {
QUrl url = QUrl(parser.value(urlOption));
if (url.isValid() && url.scheme() == HIFI_URL_SCHEME) {
if (url.isValid() && url.scheme() == URL_SCHEME_HIFI) {
qDebug() << "Writing URL to local socket";
socket.write(url.toString().toUtf8());
if (!socket.waitForBytesWritten(5000)) {

View file

@ -69,11 +69,11 @@ void AssetMappingsScriptingInterface::getMapping(QString path, QJSValue callback
void AssetMappingsScriptingInterface::uploadFile(QString path, QString mapping, QJSValue startedCallback, QJSValue completedCallback, bool dropEvent) {
static const QString helpText =
"Upload your asset to a specific folder by entering the full path. Specifying\n"
"Upload your asset to a specific folder by entering the full path. Specifying "
"a new folder name will automatically create that folder for you.";
static const QString dropHelpText =
"This file will be added to your Asset Server.\n"
"Use the field below to place your file in a specific folder or to rename it.\n"
"Use the field below to place your file in a specific folder or to rename it. "
"Specifying a new folder name will automatically create that folder for you.";
auto offscreenUi = DependencyManager::get<OffscreenUi>();

View file

@ -124,7 +124,7 @@ void WindowScriptingInterface::promptAsync(const QString& message, const QString
}
void WindowScriptingInterface::disconnectedFromDomain() {
emit domainChanged("");
emit domainChanged(QUrl());
}
QString fixupPathForMac(const QString& directory) {

View file

@ -524,7 +524,7 @@ signals:
* Triggered when you change the domain you're visiting. <strong>Warning:</strong> Is not emitted if you go to domain that
* isn't running.
* @function Window.domainChanged
* @param {string} domain - The domain's IP address.
* @param {string} domainURL - The domain's URL.
* @returns {Signal}
* @example <caption>Report when you change domains.</caption>
* function onDomainChanged(domain) {
@ -533,7 +533,7 @@ signals:
*
* Window.domainChanged.connect(onDomainChanged);
*/
void domainChanged(const QString& domain);
void domainChanged(QUrl domainURL);
/**jsdoc
* Triggered when you try to navigate to a *.json, *.svo, or *.svo.json URL in a Web browser within Interface.

View file

@ -45,7 +45,6 @@ AddressBarDialog::AddressBarDialog(QQuickItem* parent) : OffscreenQmlDialog(pare
connect(&domainHandler, &DomainHandler::connectedToDomain, this, &AddressBarDialog::hostChanged);
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &AddressBarDialog::hostChanged);
connect(DependencyManager::get<DialogsManager>().data(), &DialogsManager::setUseFeed, this, &AddressBarDialog::setUseFeed);
connect(qApp, &Application::receivedHifiSchemeURL, this, &AddressBarDialog::receivedHifiSchemeURL);
}
void AddressBarDialog::loadAddress(const QString& address, bool fromSuggestions) {

View file

@ -36,7 +36,6 @@ signals:
void backEnabledChanged();
void forwardEnabledChanged();
void useFeedChanged();
void receivedHifiSchemeURL(const QString& url);
void hostChanged();
protected:

View file

@ -882,6 +882,11 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar
//virtual
const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) {
#ifdef Q_OS_ANDROID
// disable IK on android
return underPoses;
#endif
// allows solutionSource to be overridden by an animVar
auto solutionSource = animVars.lookup(_solutionSourceVar, (int)_solutionSource);

View file

@ -221,8 +221,12 @@ void Avatar::updateAvatarEntities() {
return;
}
if (getID() == QUuid() || getID() == AVATAR_SELF_ID) {
return; // wait until MyAvatar gets an ID before doing this.
if (getID().isNull() ||
getID() == AVATAR_SELF_ID ||
DependencyManager::get<NodeList>()->getSessionUUID() == QUuid()) {
// wait until MyAvatar and this Node gets an ID before doing this. Otherwise, various things go wrong --
// things get their parent fixed up from AVATAR_SELF_ID to a null uuid which means "no parent".
return;
}
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
@ -1806,4 +1810,4 @@ scriptable::ScriptableModelBase Avatar::getScriptableModel() {
result.appendMaterials(_materials);
}
return result;
}
}

View file

@ -190,7 +190,7 @@ bool JSBaker::handleMultiLineComments(QTextStream& in) {
while (!in.atEnd()) {
in >> character;
if (character == '*') {
if (in.read(1) == '/') {
if (in.read(1) == "/") {
return true;
}
}
@ -228,7 +228,7 @@ bool JSBaker::isSpecialCharacter(QChar c) {
// If previous character is a special character, maybe don't omit new line (depends on next character as well)
bool JSBaker::isSpecialCharacterPrevious(QChar c) {
return (c == '\'' || c == '$' || c == '_' || c == '}' || c == ']' || c == ')' || c == '+' || c == '-'
|| c == '"' || c == "'");
|| c == '"' || c == '\'');
}
// If next character is a special character, maybe don't omit new line (depends on previous character as well)
@ -243,5 +243,5 @@ bool JSBaker::isSpaceOrTab(QChar c) {
// Check If the currentCharacter is " or ' or `
bool JSBaker::isQuote(QChar c) {
return (c == '"' || c == "'" || c == '`');
return (c == '"' || c == '\'' || c == '`');
}

View file

@ -73,7 +73,8 @@ void UserInputMapper::registerDevice(InputDevice::Pointer device) {
qCDebug(controllers) << "Registered input device <" << device->getName() << "> deviceID = " << deviceID;
for (const auto& inputMapping : device->getAvailableInputs()) {
auto inputs = device->getAvailableInputs();
for (const auto& inputMapping : inputs) {
const auto& input = inputMapping.first;
// Ignore aliases
if (_endpointsByInput.count(input)) {
@ -126,7 +127,8 @@ void UserInputMapper::removeDevice(int deviceID) {
_mappingsByDevice.erase(mappingsEntry);
}
for (const auto& inputMapping : device->getAvailableInputs()) {
auto inputs = device->getAvailableInputs();
for (const auto& inputMapping : inputs) {
const auto& input = inputMapping.first;
auto endpoint = _endpointsByInput.find(input);
if (endpoint != _endpointsByInput.end()) {
@ -171,7 +173,7 @@ InputDevice::Pointer UserInputMapper::getDevice(const Input& input) {
}
}
QString UserInputMapper::getDeviceName(uint16 deviceID) {
QString UserInputMapper::getDeviceName(uint16 deviceID) {
Locker locker(_lock);
if (_registeredDevices.find(deviceID) != _registeredDevices.end()) {
return _registeredDevices[deviceID]->_name;
@ -181,7 +183,7 @@ QString UserInputMapper::getDeviceName(uint16 deviceID) {
int UserInputMapper::findDevice(QString name) const {
Locker locker(_lock);
for (auto device : _registeredDevices) {
for (const auto& device : _registeredDevices) {
if (device.second->_name == name) {
return device.first;
}
@ -192,7 +194,7 @@ int UserInputMapper::findDevice(QString name) const {
QVector<QString> UserInputMapper::getDeviceNames() {
Locker locker(_lock);
QVector<QString> result;
for (auto device : _registeredDevices) {
for (const auto& device : _registeredDevices) {
QString deviceName = device.second->_name.split(" (")[0];
result << deviceName;
}
@ -218,7 +220,7 @@ Input UserInputMapper::findDeviceInput(const QString& inputName) const {
const auto& device = _registeredDevices.at(deviceID);
auto deviceInputs = device->getAvailableInputs();
for (auto input : deviceInputs) {
for (const auto& input : deviceInputs) {
if (input.second == inputName) {
return input.first;
}
@ -321,7 +323,8 @@ QVector<Action> UserInputMapper::getAllActions() const {
QString UserInputMapper::getActionName(Action action) const {
Locker locker(_lock);
for (auto actionPair : getActionInputs()) {
auto inputs = getActionInputs();
for (const auto& actionPair : inputs) {
if (actionPair.first.channel == toInt(action)) {
return actionPair.second;
}
@ -331,18 +334,20 @@ QString UserInputMapper::getActionName(Action action) const {
QString UserInputMapper::getStandardPoseName(uint16_t pose) {
Locker locker(_lock);
for (auto posePair : getStandardInputs()) {
auto inputs = getStandardInputs();
for (const auto& posePair : inputs) {
if (posePair.first.channel == pose && posePair.first.getType() == ChannelType::POSE) {
return posePair.second;
}
}
return QString();
}
}
QVector<QString> UserInputMapper::getActionNames() const {
Locker locker(_lock);
QVector<QString> result;
for (auto actionPair : getActionInputs()) {
auto inputs = getActionInputs();
for (const auto& actionPair : inputs) {
result << actionPair.second;
}
return result;
@ -357,7 +362,7 @@ Pose UserInputMapper::getPoseState(Action action) const {
bool UserInputMapper::triggerHapticPulse(float strength, float duration, controller::Hand hand) {
Locker locker(_lock);
bool toReturn = false;
for (auto device : _registeredDevices) {
for (const auto& device : _registeredDevices) {
toReturn = toReturn || device.second->triggerHapticPulse(strength, duration, hand);
}
return toReturn;
@ -469,7 +474,7 @@ void UserInputMapper::runMappings() {
if (debugRoutes) {
qCDebug(controllers) << "Beginning mapping frame";
}
for (auto endpointEntry : this->_endpointsByInput) {
for (const auto& endpointEntry : _endpointsByInput) {
endpointEntry.second->reset();
}
@ -542,9 +547,9 @@ bool UserInputMapper::applyRoute(const Route::Pointer& route, bool force) {
}
// Most endpoints can only be read once (though a given mapping can route them to
// Most endpoints can only be read once (though a given mapping can route them to
// multiple places). Consider... If the default is to wire the A button to JUMP
// and someone else wires it to CONTEXT_MENU, I don't want both to occur when
// and someone else wires it to CONTEXT_MENU, I don't want both to occur when
// I press the button. The exception is if I'm wiring a control back to itself
// in order to adjust my interface, like inverting the Y axis on an analog stick
if (!route->peek && !source->readable()) {
@ -897,7 +902,8 @@ Conditional::Pointer UserInputMapper::parseConditional(const QJsonValue& value)
if (value.isArray()) {
// Support "when" : [ "GamePad.RB", "GamePad.LB" ]
Conditional::List children;
for (auto arrayItem : value.toArray()) {
auto array = value.toArray();
for (const auto& arrayItem : array) {
Conditional::Pointer childConditional = parseConditional(arrayItem);
if (!childConditional) {
return Conditional::Pointer();
@ -908,7 +914,7 @@ Conditional::Pointer UserInputMapper::parseConditional(const QJsonValue& value)
} else if (value.isString()) {
// Support "when" : "GamePad.RB"
auto conditionalToken = value.toString();
// Detect for modifier case (Not...)
QString conditionalModifier;
const QString JSON_CONDITIONAL_MODIFIER_NOT("!");
@ -943,12 +949,12 @@ Filter::Pointer UserInputMapper::parseFilter(const QJsonValue& value) {
result = Filter::getFactory().create(value.toString());
} else if (value.isObject()) {
result = Filter::parse(value.toObject());
}
}
if (!result) {
qWarning() << "Invalid filter definition " << value;
}
return result;
}
@ -960,7 +966,7 @@ Filter::List UserInputMapper::parseFilters(const QJsonValue& value) {
if (value.isArray()) {
Filter::List result;
auto filtersArray = value.toArray();
for (auto filterValue : filtersArray) {
for (const auto& filterValue : filtersArray) {
Filter::Pointer filter = parseFilter(filterValue);
if (!filter) {
return Filter::List();
@ -968,7 +974,7 @@ Filter::List UserInputMapper::parseFilters(const QJsonValue& value) {
result.push_back(filter);
}
return result;
}
}
Filter::Pointer filter = parseFilter(value);
if (!filter) {
@ -980,7 +986,8 @@ Filter::List UserInputMapper::parseFilters(const QJsonValue& value) {
Endpoint::Pointer UserInputMapper::parseDestination(const QJsonValue& value) {
if (value.isArray()) {
ArrayEndpoint::Pointer result = std::make_shared<ArrayEndpoint>();
for (auto arrayItem : value.toArray()) {
auto array = value.toArray();
for (const auto& arrayItem : array) {
Endpoint::Pointer destination = parseEndpoint(arrayItem);
if (!destination) {
return Endpoint::Pointer();
@ -988,14 +995,14 @@ Endpoint::Pointer UserInputMapper::parseDestination(const QJsonValue& value) {
result->_children.push_back(destination);
}
return result;
}
}
return parseEndpoint(value);
}
Endpoint::Pointer UserInputMapper::parseAxis(const QJsonValue& value) {
if (value.isObject()) {
auto object = value.toObject();
auto object = value.toObject();
if (object.contains("makeAxis")) {
auto axisValue = object.value("makeAxis");
if (axisValue.isArray()) {
@ -1017,7 +1024,8 @@ Endpoint::Pointer UserInputMapper::parseAxis(const QJsonValue& value) {
Endpoint::Pointer UserInputMapper::parseAny(const QJsonValue& value) {
if (value.isArray()) {
Endpoint::List children;
for (auto arrayItem : value.toArray()) {
auto array = value.toArray();
for (const auto& arrayItem : array) {
Endpoint::Pointer destination = parseEndpoint(arrayItem);
if (!destination) {
return Endpoint::Pointer();
@ -1162,7 +1170,7 @@ Mapping::Pointer UserInputMapper::parseMapping(const QString& json) {
template <typename T>
bool hasDebuggableRoute(const T& routes) {
for (auto route : routes) {
for (const auto& route : routes) {
if (route->debug) {
return true;
}
@ -1174,7 +1182,7 @@ bool hasDebuggableRoute(const T& routes) {
void UserInputMapper::enableMapping(const Mapping::Pointer& mapping) {
Locker locker(_lock);
// New routes for a device get injected IN FRONT of existing routes. Routes
// are processed in order so this ensures that the standard -> action processing
// are processed in order so this ensures that the standard -> action processing
// takes place after all of the hardware -> standard or hardware -> action processing
// because standard -> action is the first set of routes added.
Route::List standardRoutes = mapping->routes;

View file

@ -16,11 +16,14 @@
using namespace controller;
void ActionEndpoint::apply(float newValue, const Pointer& source) {
InputRecorder* inputRecorder = InputRecorder::getInstance();
auto userInputMapper = DependencyManager::get<UserInputMapper>();
QString actionName = userInputMapper->getActionName(Action(_input.getChannel()));
if(inputRecorder->isPlayingback()) {
newValue = inputRecorder->getActionState(actionName);
InputRecorder* inputRecorder = InputRecorder::getInstance();
QString actionName;
if (inputRecorder->isPlayingback() || inputRecorder->isRecording()) {
actionName = userInputMapper->getActionName(Action(_input.getChannel()));
if (inputRecorder->isPlayingback()) {
newValue = inputRecorder->getActionState(actionName);
}
}
_currentValue += newValue;
@ -32,10 +35,12 @@ void ActionEndpoint::apply(float newValue, const Pointer& source) {
void ActionEndpoint::apply(const Pose& value, const Pointer& source) {
_currentPose = value;
InputRecorder* inputRecorder = InputRecorder::getInstance();
auto userInputMapper = DependencyManager::get<UserInputMapper>();
QString actionName = userInputMapper->getActionName(Action(_input.getChannel()));
inputRecorder->setActionState(actionName, _currentPose);
InputRecorder* inputRecorder = InputRecorder::getInstance();
if (inputRecorder->isRecording()) {
QString actionName = userInputMapper->getActionName(Action(_input.getChannel()));
inputRecorder->setActionState(actionName, _currentPose);
}
if (!_currentPose.isValid()) {
return;

View file

@ -55,7 +55,7 @@ HTTPConnection::~HTTPConnection() {
QHash<QString, QString> HTTPConnection::parseUrlEncodedForm() {
// make sure we have the correct MIME type
QList<QByteArray> elements = _requestHeaders.value("Content-Type").split(';');
QList<QByteArray> elements = requestHeader("Content-Type").split(';');
QString contentType = elements.at(0).trimmed();
if (contentType != "application/x-www-form-urlencoded") {
@ -75,7 +75,7 @@ QHash<QString, QString> HTTPConnection::parseUrlEncodedForm() {
QList<FormData> HTTPConnection::parseFormData() const {
// make sure we have the correct MIME type
QList<QByteArray> elements = _requestHeaders.value("Content-Type").split(';');
QList<QByteArray> elements = requestHeader("Content-Type").split(';');
QString contentType = elements.at(0).trimmed();
@ -251,7 +251,7 @@ void HTTPConnection::readHeaders() {
if (trimmed.isEmpty()) {
_socket->disconnect(this, SLOT(readHeaders()));
QByteArray clength = _requestHeaders.value("Content-Length");
QByteArray clength = requestHeader("Content-Length");
if (clength.isEmpty()) {
_parentManager->handleHTTPRequest(this, _requestUrl);
@ -275,7 +275,7 @@ void HTTPConnection::readHeaders() {
respond("400 Bad Request", "The header was malformed.");
return;
}
_lastRequestHeader = trimmed.left(idx);
_lastRequestHeader = trimmed.left(idx).toLower();
QByteArray& value = _requestHeaders[_lastRequestHeader];
if (!value.isEmpty()) {
value.append(", ");

View file

@ -72,8 +72,8 @@ public:
/// Returns a reference to the request URL.
const QUrl& requestUrl () const { return _requestUrl; }
/// Returns a reference to the request headers.
const Headers& requestHeaders () const { return _requestHeaders; }
/// Returns a copy of the request header value. If it does not exist, it will return a default constructed QByteArray.
QByteArray requestHeader(const QString& key) const { return _requestHeaders.value(key.toLower().toLocal8Bit()); }
/// Returns a reference to the request content.
const QByteArray& requestContent () const { return _requestContent; }

View file

@ -91,6 +91,11 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
return;
}
if (entityTree && entityTree->isServerlessMode()) {
// if we are in a serverless domain, don't send edit packets
return;
}
QByteArray bufferOut(NLPacket::maxPayloadSize(type), 0);
if (type == PacketType::EntityAdd) {

View file

@ -963,7 +963,11 @@ void EntityItem::setHref(QString value) {
// If the string has something and doesn't start with with "hifi://" it shouldn't be set
// We allow the string to be empty, because that's the initial state of this property
if ( !(value.toLower().startsWith("hifi://")) && !value.isEmpty()) {
if (!value.isEmpty() &&
!(value.toLower().startsWith("hifi://")) &&
!(value.toLower().startsWith("file://"))
// TODO: serverless-domains will eventually support http and https also
) {
return;
}
withWriteLock([&] {

View file

@ -451,8 +451,11 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
* @property {Entities.EntityType} type - The entity type. You cannot change the type of an entity after it's created. (Though
* its value may switch among <code>"Box"</code>, <code>"Shape"</code>, and <code>"Sphere"</code> depending on changes to
* the <code>shape</code> property set for entities of these types.) <em>Read-only.</em>
* @property {boolean} clientOnly=false - If <code>true</code> then the entity is an avatar entity, otherwise it is a server
* entity. <em>Read-only.</em>
* @property {boolean} clientOnly=false - If <code>true</code> then the entity is an avatar entity; otherwise it is a server
* entity. An avatar entity follows you to each domain you visit, rendering at the same world coordinates unless it's
* parented to your avatar. <em>Value cannot be changed after the entity is created.</em><br />
* The value can also be set at entity creation by using the <code>clientOnly</code> parameter in
* {@link Entities.addEntity}.
* @property {Uuid} owningAvatarID=Uuid.NULL - The session ID of the owning avatar if <code>clientOnly</code> is
* <code>true</code>, otherwise {@link Uuid|Uuid.NULL}. <em>Read-only.</em>
*
@ -1413,7 +1416,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_ANGULAR_VELOCITY, localAngularVelocity);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_DIMENSIONS, localDimensions);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLIENT_ONLY, clientOnly); // Gettable but not settable
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLIENT_ONLY, clientOnly); // Gettable but not settable except at entity creation
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_OWNING_AVATAR_ID, owningAvatarID); // Gettable but not settable
// Rendering info
@ -2864,6 +2867,9 @@ void EntityItemProperties::markAllChanged() {
_ambientLight.markAllChanged();
_skybox.markAllChanged();
_keyLightModeChanged = true;
_skyboxModeChanged = true;
_ambientLightModeChanged = true;
_hazeModeChanged = true;
_animation.markAllChanged();

View file

@ -596,7 +596,7 @@ void EntityScriptingInterface::deleteEntity(QUuid id) {
shouldDelete = false;
} else {
// only delete local entities, server entities will round trip through the server filters
if (entity->getClientOnly()) {
if (entity->getClientOnly() || _entityTree->isServerlessMode()) {
_entityTree->deleteEntity(entityID);
}
}
@ -1285,10 +1285,10 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID,
}
doTransmit = actor(simulation, entity);
_entityTree->entityChanged(entity);
if (doTransmit) {
properties.setClientOnly(entity->getClientOnly());
properties.setOwningAvatarID(entity->getOwningAvatarID());
_entityTree->entityChanged(entity);
}
});

View file

@ -203,9 +203,9 @@ public slots:
* Add a new entity with specified properties.
* @function Entities.addEntity
* @param {Entities.EntityProperties} properties - The properties of the entity to create.
* @param {boolean} [clientOnly=false] - If <code>true</code>, the entity is created as an avatar entity, otherwise it
* is created on the server. An avatar entity follows you to each domain you visit, rendering at the same world
* coordinates unless it's parented to your avatar.
* @param {boolean} [clientOnly=false] - If <code>true</code>, or if <code>clientOnly</code> is set <code>true</code> in
* the properties, the entity is created as an avatar entity; otherwise it is created on the server. An avatar entity
* follows you to each domain you visit, rendering at the same world coordinates unless it's parented to your avatar.
* @returns {Uuid} The ID of the entity if successfully created, otherwise {@link Uuid|Uuid.NULL}.
* @example <caption>Create a box entity in front of your avatar.</caption>
* var entityID = Entities.addEntity({

View file

@ -493,7 +493,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti
if (!properties.getClientOnly() && getIsClient() &&
!nodeList->getThisNodeCanRez() && !nodeList->getThisNodeCanRezTmp() &&
!nodeList->getThisNodeCanRezCertified() && !nodeList->getThisNodeCanRezTmpCertified()) {
!nodeList->getThisNodeCanRezCertified() && !nodeList->getThisNodeCanRezTmpCertified() && !_serverlessDomain) {
return nullptr;
}
@ -1509,7 +1509,8 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
}
if (isAdd && properties.getLocked() && !senderNode->isAllowedEditor()) {
// if a node can't change locks, don't allow them to create an already-locked entity
// if a node can't change locks, don't allow it to create an already-locked entity -- automatically
// clear the locked property and allow the unlocked entity to be created.
properties.setLocked(false);
bumpTimestamp(properties);
}
@ -2181,23 +2182,25 @@ QVector<EntityItemID> EntityTree::sendEntities(EntityEditPacketSender* packetSen
localTree->recurseTreeWithOperator(&moveOperator);
}
// send add-entity packets to the server
i = map.begin();
while (i != map.end()) {
EntityItemID newID = i.value();
EntityItemPointer entity = localTree->findEntityByEntityItemID(newID);
if (entity) {
// queue the packet to send to the server
entity->updateQueryAACube();
EntityItemProperties properties = entity->getProperties();
properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity
packetSender->queueEditEntityMessage(PacketType::EntityAdd, localTree, newID, properties);
i++;
} else {
i = map.erase(i);
if (!_serverlessDomain) {
// send add-entity packets to the server
i = map.begin();
while (i != map.end()) {
EntityItemID newID = i.value();
EntityItemPointer entity = localTree->findEntityByEntityItemID(newID);
if (entity) {
// queue the packet to send to the server
entity->updateQueryAACube();
EntityItemProperties properties = entity->getProperties();
properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity
packetSender->queueEditEntityMessage(PacketType::EntityAdd, localTree, newID, properties);
i++;
} else {
i = map.erase(i);
}
}
packetSender->releaseQueuedMessages();
}
packetSender->releaseQueuedMessages();
return map.values().toVector();
}

View file

@ -283,6 +283,9 @@ public:
void setMyAvatar(std::shared_ptr<AvatarData> myAvatar) { _myAvatar = myAvatar; }
void setIsServerlessMode(bool value) { _serverlessDomain = value; }
bool isServerlessMode() const { return _serverlessDomain; }
static void setAddMaterialToEntityOperator(std::function<bool(const QUuid&, graphics::MaterialLayer, const std::string&)> addMaterialToEntityOperator) { _addMaterialToEntityOperator = addMaterialToEntityOperator; }
static void setRemoveMaterialFromEntityOperator(std::function<bool(const QUuid&, graphics::MaterialPointer, const std::string&)> removeMaterialFromEntityOperator) { _removeMaterialFromEntityOperator = removeMaterialFromEntityOperator; }
static bool addMaterialToEntity(const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName);
@ -325,7 +328,7 @@ protected:
void notifyNewlyCreatedEntity(const EntityItem& newEntity, const SharedNodePointer& senderNode);
bool isScriptInWhitelist(const QString& scriptURL);
QReadWriteLock _newlyCreatedHooksLock;
QVector<NewlyCreatedEntityHook*> _newlyCreatedHooks;
@ -412,6 +415,8 @@ private:
static std::function<bool(const QUuid&, graphics::MaterialPointer, const std::string&)> _removeMaterialFromAvatarOperator;
static std::function<bool(const QUuid&, graphics::MaterialLayer, const std::string&)> _addMaterialToOverlayOperator;
static std::function<bool(const QUuid&, graphics::MaterialPointer, const std::string&)> _removeMaterialFromOverlayOperator;
bool _serverlessDomain { false };
};
#endif // hifi_EntityTree_h

View file

@ -38,11 +38,13 @@
#include <NumericalConstants.h>
#include <shared/NsightHelpers.h>
#include <shared/FileUtils.h>
#include <PathUtils.h>
#include <Finally.h>
#include <Profile.h>
#include "NetworkLogging.h"
#include "ModelNetworkingLogging.h"
#include "NetworkingConstants.h"
#include <Trace.h>
#include <StatTracker.h>
@ -467,7 +469,7 @@ void NetworkTexture::makeLocalRequest() {
const QString scheme = _url.scheme();
QString path;
if (scheme == URL_SCHEME_FILE) {
path = _url.toLocalFile();
path = PathUtils::expandToLocalDataAbsolutePath(_url).toLocalFile();
} else {
path = ":" + _url.path();
}

View file

@ -22,6 +22,7 @@
#include <NumericalConstants.h>
#include <SettingHandle.h>
#include <UUID.h>
#include <PathUtils.h>
#include "AddressManager.h"
#include "NodeList.h"
@ -40,27 +41,14 @@ const QString SETTINGS_CURRENT_ADDRESS_KEY = "address";
Setting::Handle<QUrl> currentAddressHandle(QStringList() << ADDRESS_MANAGER_SETTINGS_GROUP << "address", DEFAULT_HIFI_ADDRESS);
AddressManager::AddressManager() :
_port(0)
{
}
bool AddressManager::isConnected() {
return DependencyManager::get<NodeList>()->getDomainHandler().isConnected();
}
QUrl AddressManager::currentAddress(bool domainOnly) const {
QUrl hifiURL;
QUrl hifiURL = _domainURL;
hifiURL.setScheme(HIFI_URL_SCHEME);
hifiURL.setHost(_host);
if (_port != 0 && _port != DEFAULT_DOMAIN_SERVER_PORT) {
hifiURL.setPort(_port);
}
if (!domainOnly) {
if (!domainOnly && hifiURL.scheme() == URL_SCHEME_HIFI) {
hifiURL.setPath(currentPath());
}
@ -69,7 +57,9 @@ QUrl AddressManager::currentAddress(bool domainOnly) const {
QUrl AddressManager::currentFacingAddress() const {
auto hifiURL = currentAddress();
hifiURL.setPath(currentFacingPath());
if (hifiURL.scheme() == URL_SCHEME_HIFI) {
hifiURL.setPath(currentFacingPath());
}
return hifiURL;
}
@ -79,7 +69,7 @@ QUrl AddressManager::currentShareableAddress(bool domainOnly) const {
// if we have a shareable place name use that instead of whatever the current host is
QUrl hifiURL;
hifiURL.setScheme(HIFI_URL_SCHEME);
hifiURL.setScheme(URL_SCHEME_HIFI);
hifiURL.setHost(_shareablePlaceName);
if (!domainOnly) {
@ -94,7 +84,9 @@ QUrl AddressManager::currentShareableAddress(bool domainOnly) const {
QUrl AddressManager::currentFacingShareableAddress() const {
auto hifiURL = currentShareableAddress();
hifiURL.setPath(currentFacingPath());
if (hifiURL.scheme() == URL_SCHEME_HIFI) {
hifiURL.setPath(currentFacingPath());
}
return hifiURL;
}
@ -137,11 +129,16 @@ void AddressManager::goForward() {
void AddressManager::storeCurrentAddress() {
auto url = currentAddress();
if (!url.host().isEmpty()) {
if (url.scheme() == URL_SCHEME_FILE ||
(url.scheme() == URL_SCHEME_HIFI && !url.host().isEmpty())) {
// TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can
// be loaded over http(s)
// url.scheme() == URL_SCHEME_HTTP ||
// url.scheme() == URL_SCHEME_HTTPS ||
currentAddressHandle.set(url);
} else {
qCWarning(networking) << "Ignoring attempt to save current address with an empty host" << url;
qCWarning(networking) << "Ignoring attempt to save current address with an invalid url:" << url;
}
}
@ -207,7 +204,7 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) {
static QString URL_TYPE_DOMAIN_ID = "domain_id";
static QString URL_TYPE_PLACE = "place";
static QString URL_TYPE_NETWORK_ADDRESS = "network_address";
if (lookupUrl.scheme() == HIFI_URL_SCHEME) {
if (lookupUrl.scheme() == URL_SCHEME_HIFI) {
qCDebug(networking) << "Trying to go to URL" << lookupUrl.toString();
@ -287,12 +284,36 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) {
handlePath(lookupUrl.path(), trigger, true);
emit lookupResultsFinished();
return true;
} else if (lookupUrl.scheme() == URL_SCHEME_FILE) {
// TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can
// be loaded over http(s)
// lookupUrl.scheme() == URL_SCHEME_HTTP ||
// lookupUrl.scheme() == URL_SCHEME_HTTPS ||
_previousLookup.clear();
QUrl domainURL = PathUtils::expandToLocalDataAbsolutePath(lookupUrl);
setDomainInfo(domainURL, trigger);
emit lookupResultsFinished();
handlePath(DOMAIN_SPAWNING_POINT, LookupTrigger::Internal, false);
return true;
}
return false;
}
bool isPossiblePlaceName(QString possiblePlaceName) {
bool result { false };
int length = possiblePlaceName.length();
static const int MINIMUM_PLACENAME_LENGTH = 1;
static const int MAXIMUM_PLACENAME_LENGTH = 64;
if (possiblePlaceName.toLower() != "localhost" &&
length >= MINIMUM_PLACENAME_LENGTH && length <= MAXIMUM_PLACENAME_LENGTH) {
const QRegExp PLACE_NAME_REGEX = QRegExp("^[0-9A-Za-z](([0-9A-Za-z]|-(?!-))*[^\\W_]$|$)");
result = PLACE_NAME_REGEX.indexIn(possiblePlaceName) == 0;
}
return result;
}
void AddressManager::handleLookupString(const QString& lookupString, bool fromSuggestions) {
if (!lookupString.isEmpty()) {
// make this a valid hifi URL and handle it off to handleUrl
@ -300,12 +321,16 @@ void AddressManager::handleLookupString(const QString& lookupString, bool fromSu
QUrl lookupURL;
if (!lookupString.startsWith('/')) {
const QRegExp HIFI_SCHEME_REGEX = QRegExp(HIFI_URL_SCHEME + ":\\/{1,2}", Qt::CaseInsensitive);
// sometimes we need to handle lookupStrings like hifi:/somewhere
const QRegExp HIFI_SCHEME_REGEX = QRegExp(URL_SCHEME_HIFI + ":\\/{1,2}", Qt::CaseInsensitive);
sanitizedString = sanitizedString.remove(HIFI_SCHEME_REGEX);
lookupURL = QUrl(HIFI_URL_SCHEME + "://" + sanitizedString);
lookupURL = QUrl(sanitizedString);
if (lookupURL.scheme().isEmpty()) {
lookupURL = QUrl("hifi://" + sanitizedString);
}
} else {
lookupURL = QUrl(lookupString);
lookupURL = QUrl(sanitizedString);
}
handleUrl(lookupURL, fromSuggestions ? Suggestions : UserInput);
@ -383,7 +408,11 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const
qCDebug(networking) << "Possible domain change required to connect to" << domainHostname
<< "on" << domainPort;
emit possibleDomainChangeRequired(domainHostname, domainPort, domainID);
QUrl domainURL;
domainURL.setScheme(URL_SCHEME_HIFI);
domainURL.setHost(domainHostname);
domainURL.setPort(domainPort);
emit possibleDomainChangeRequired(domainURL, domainID);
} else {
QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString();
@ -420,15 +449,10 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const
if (setHost(placeName, trigger)) {
trigger = LookupTrigger::Internal;
}
_placeName = placeName;
} else {
if (setHost(domainIDString, trigger)) {
trigger = LookupTrigger::Internal;
}
// this isn't a place, so clear the place name
_placeName.clear();
}
// check if we had a path to override the path returned
@ -549,13 +573,17 @@ bool AddressManager::handleNetworkAddress(const QString& lookupString, LookupTri
if (ipAddressRegex.indexIn(lookupString) != -1) {
QString domainIPString = ipAddressRegex.cap(1);
qint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT;
quint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT;
if (!ipAddressRegex.cap(2).isEmpty()) {
domainPort = (qint16) ipAddressRegex.cap(2).toInt();
domainPort = (quint16) ipAddressRegex.cap(2).toInt();
}
emit lookupResultsFinished();
hostChanged = setDomainInfo(domainIPString, domainPort, trigger);
QUrl domainURL;
domainURL.setScheme(URL_SCHEME_HIFI);
domainURL.setHost(domainIPString);
domainURL.setPort(domainPort);
hostChanged = setDomainInfo(domainURL, trigger);
return true;
}
@ -568,11 +596,15 @@ bool AddressManager::handleNetworkAddress(const QString& lookupString, LookupTri
quint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT;
if (!hostnameRegex.cap(2).isEmpty()) {
domainPort = (qint16)hostnameRegex.cap(2).toInt();
domainPort = (quint16)hostnameRegex.cap(2).toInt();
}
emit lookupResultsFinished();
hostChanged = setDomainInfo(domainHostname, domainPort, trigger);
QUrl domainURL;
domainURL.setScheme(URL_SCHEME_HIFI);
domainURL.setHost(domainHostname);
domainURL.setPort(domainPort);
hostChanged = setDomainInfo(domainURL, trigger);
return true;
}
@ -641,7 +673,7 @@ bool AddressManager::handleViewpoint(const QString& viewpointString, bool should
addCurrentAddressToHistory(trigger);
}
if (!isNaN(newPosition.x) && !isNaN(newPosition.y) && !isNaN(newPosition.z)) {
if (!isNaN(newPosition)) {
glm::quat newOrientation;
QRegExp orientationRegex(QUAT_REGEX_STRING);
@ -661,11 +693,11 @@ bool AddressManager::handleViewpoint(const QString& viewpointString, bool should
&& !isNaN(newOrientation.w)) {
orientationChanged = true;
} else {
qCDebug(networking) << "Orientation parsed from lookup string is invalid. Will not use for location change.";
qCDebug(networking) << "Orientation parsed from lookup string is invalid. Won't use for location change.";
}
}
emit locationChangeRequired(newPosition, orientationChanged,
emit locationChangeRequired(newPosition, orientationChanged,
trigger == LookupTrigger::VisitUserFromPAL ? cancelOutRollAndPitch(newOrientation): newOrientation,
shouldFace
);
@ -696,18 +728,20 @@ bool AddressManager::handleUsername(const QString& lookupString) {
}
bool AddressManager::setHost(const QString& host, LookupTrigger trigger, quint16 port) {
if (host != _host || port != _port) {
if (host != _domainURL.host() || port != _domainURL.port()) {
addCurrentAddressToHistory(trigger);
_port = port;
bool emitHostChanged = host != _domainURL.host();
_domainURL = QUrl();
_domainURL.setScheme(URL_SCHEME_HIFI);
_domainURL.setHost(host);
_domainURL.setPort(port);
// any host change should clear the shareable place name
_shareablePlaceName.clear();
if (host != _host) {
_host = host;
emit hostChanged(_host);
if (emitHostChanged) {
emit hostChanged(host);
}
return true;
@ -716,20 +750,43 @@ bool AddressManager::setHost(const QString& host, LookupTrigger trigger, quint16
return false;
}
bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, LookupTrigger trigger) {
bool hostChanged = setHost(hostname, trigger, port);
QString AddressManager::getHost() const {
if (isPossiblePlaceName(_domainURL.host())) {
return QString();
}
return _domainURL.host();
}
bool AddressManager::setDomainInfo(const QUrl& domainURL, LookupTrigger trigger) {
const QString hostname = domainURL.host();
quint16 port = domainURL.port();
bool emitHostChanged { false };
if (domainURL != _domainURL) {
addCurrentAddressToHistory(trigger);
emitHostChanged = true;
}
_domainURL = domainURL;
// clear any current place information
_rootPlaceID = QUuid();
_placeName.clear();
qCDebug(networking) << "Possible domain change required to connect to domain at" << hostname << "on" << port;
if (_domainURL.scheme() == URL_SCHEME_HIFI) {
qCDebug(networking) << "Possible domain change required to connect to domain at" << hostname << "on" << port;
} else {
qCDebug(networking) << "Possible domain change required to serverless domain: " << domainURL.toString();
}
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::HandleAddress);
emit possibleDomainChangeRequired(hostname, port, QUuid());
if (emitHostChanged) {
emit hostChanged(domainURL.host());
}
emit possibleDomainChangeRequired(_domainURL, QUuid());
return hostChanged;
return emitHostChanged;
}
void AddressManager::goToUser(const QString& username, bool shouldMatchOrientation) {
@ -818,7 +875,7 @@ void AddressManager::lookupShareableNameForDomainID(const QUuid& domainID) {
// then use that for Steam join/invite or copiable address
// it only makes sense to lookup a shareable default name if we don't have a place name
if (_placeName.isEmpty()) {
if (getPlaceName().isEmpty()) {
JSONCallbackParameters callbackParams;
// no error callback handling
@ -870,3 +927,12 @@ void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) {
}
}
QString AddressManager::getPlaceName() const {
if (!_shareablePlaceName.isEmpty()) {
return _shareablePlaceName;
}
if (isPossiblePlaceName(_domainURL.host())) {
return _domainURL.host();
}
return QString();
}

View file

@ -22,8 +22,6 @@
#include "AccountManager.h"
const QString HIFI_URL_SCHEME = "hifi";
extern const QString DEFAULT_HIFI_ADDRESS;
const QString SANDBOX_HIFI_ADDRESS = "hifi://localhost";
@ -147,7 +145,7 @@ public:
};
bool isConnected();
const QString& getProtocol() { return HIFI_URL_SCHEME; };
const QString& getProtocol() { return URL_SCHEME_HIFI; };
QUrl currentAddress(bool domainOnly = false) const;
QUrl currentFacingAddress() const;
@ -157,10 +155,10 @@ public:
QString currentFacingPath() const;
const QUuid& getRootPlaceID() const { return _rootPlaceID; }
const QString& getPlaceName() const { return _shareablePlaceName.isEmpty() ? _placeName : _shareablePlaceName; }
QString getPlaceName() const;
QString getDomainID() const;
const QString& getHost() const { return _host; }
QString getHost() const;
void setPositionGetter(PositionGetter positionGetter) { _positionGetter = positionGetter; }
void setOrientationGetter(OrientationGetter orientationGetter) { _orientationGetter = orientationGetter; }
@ -170,6 +168,8 @@ public:
const QStack<QUrl>& getBackStack() const { return _backStack; }
const QStack<QUrl>& getForwardStack() const { return _forwardStack; }
QUrl getDomainURL() { return _domainURL; }
public slots:
/**jsdoc
* Go to a specified metaverse address.
@ -302,13 +302,12 @@ signals:
/**jsdoc
* Triggered when a request is made to go to an IP address.
* @function location.possibleDomainChangeRequired
* @param {string} hostName - The name of the domain to go do.
* @param {number} port - The integer number of the network port to connect to.
* @param {Url} domainURL - URL for domain
* @param {Uuid} domainID - The UUID of the domain to go to.
* @returns {Signal}
*/
// No example because this function isn't typically used in scripts.
void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort, const QUuid& domainID);
void possibleDomainChangeRequired(QUrl domainURL, QUuid domainID);
/**jsdoc
* Triggered when a request is made to go to a named domain or user.
@ -360,7 +359,7 @@ signals:
* location.pathChangeRequired.connect(onPathChangeRequired);
*/
void pathChangeRequired(const QString& newPath);
/**jsdoc
* Triggered when you navigate to a new domain.
* @function location.hostChanged
@ -392,7 +391,7 @@ signals:
void goBackPossible(bool isPossible);
/**jsdoc
* Triggered when there's a change in whether or not there's a forward location that can be navigated to using
* Triggered when there's a change in whether or not there's a forward location that can be navigated to using
* {@link location.goForward|goForward}. (Reflects changes in the state of the "Goto" dialog's forward arrow.)
* @function location.goForwardPossible
* @param {boolean} isPossible - <code>true</code> if there's a forward location to navigate to, otherwise
@ -407,8 +406,6 @@ signals:
*/
void goForwardPossible(bool isPossible);
protected:
AddressManager();
private slots:
void handleAPIResponse(QNetworkReply& requestReply);
void handleAPIError(QNetworkReply& errorReply);
@ -420,7 +417,7 @@ private:
// Set host and port, and return `true` if it was changed.
bool setHost(const QString& host, LookupTrigger trigger, quint16 port = 0);
bool setDomainInfo(const QString& hostname, quint16 port, LookupTrigger trigger);
bool setDomainInfo(const QUrl& domainURL, LookupTrigger trigger);
const JSONCallbackParameters& apiCallbackParameters();
@ -438,9 +435,8 @@ private:
void addCurrentAddressToHistory(LookupTrigger trigger);
QString _host;
quint16 _port;
QString _placeName;
QUrl _domainURL;
QUuid _rootPlaceID;
PositionGetter _positionGetter;
OrientationGetter _orientationGetter;
@ -452,7 +448,7 @@ private:
quint64 _lastBackPush = 0;
QString _newHostLookupPath;
QUrl _previousLookup;
};

View file

@ -20,6 +20,7 @@
#include "NetworkAccessManager.h"
#include "NetworkLogging.h"
#include "NetworkingConstants.h"
#include "ResourceManager.h"

View file

@ -38,7 +38,7 @@ DomainHandler::DomainHandler(QObject* parent) :
// if we get a socket that make sure our NetworkPeer ping timer stops
connect(this, &DomainHandler::completedSocketDiscovery, &_icePeer, &NetworkPeer::stopPingTimer);
// setup a timeout for failure on settings requests
static const int DOMAIN_SETTINGS_TIMEOUT_MS = 5000;
_settingsTimer.setInterval(DOMAIN_SETTINGS_TIMEOUT_MS); // 5s, Qt::CoarseTimer acceptable
@ -60,11 +60,11 @@ void DomainHandler::disconnect() {
if (_isConnected) {
sendDisconnectPacket();
}
// clear member variables that hold the connection state to a domain
_uuid = QUuid();
_connectionToken = QUuid();
_icePeer.reset();
if (requiresICE()) {
@ -78,10 +78,10 @@ void DomainHandler::disconnect() {
void DomainHandler::sendDisconnectPacket() {
// The DomainDisconnect packet is not verified - we're relying on the eventual addition of DTLS to the
// domain-server connection to stop greifing here
// construct the disconnect packet once (an empty packet but sourced with our current session UUID)
static auto disconnectPacket = NLPacket::create(PacketType::DomainDisconnectRequest, 0);
// send the disconnect packet to the current domain server
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendUnreliablePacket(*disconnectPacket, _sockAddr);
@ -94,7 +94,7 @@ void DomainHandler::clearSettings() {
void DomainHandler::softReset() {
qCDebug(networking) << "Resetting current domain connection information.";
disconnect();
clearSettings();
_connectionDenialsSinceKeypairRegen = 0;
@ -115,8 +115,8 @@ void DomainHandler::hardReset() {
qCDebug(networking) << "Hard reset in NodeList DomainHandler.";
_pendingDomainID = QUuid();
_iceServerSockAddr = HifiSockAddr();
_hostname = QString();
_sockAddr.clear();
_domainURL = QUrl();
_domainConnectionRefusals.clear();
@ -139,7 +139,10 @@ void DomainHandler::setSockAddr(const HifiSockAddr& sockAddr, const QString& hos
}
// some callers may pass a hostname, this is not to be used for lookup but for DTLS certificate verification
_hostname = hostname;
_domainURL = QUrl();
_domainURL.setScheme(URL_SCHEME_HIFI);
_domainURL.setHost(hostname);
_domainURL.setPort(_sockAddr.getPort());
}
void DomainHandler::setUUID(const QUuid& uuid) {
@ -149,36 +152,45 @@ void DomainHandler::setUUID(const QUuid& uuid) {
}
}
void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) {
void DomainHandler::setURLAndID(QUrl domainURL, QUuid domainID) {
_pendingDomainID = domainID;
if (hostname != _hostname || _sockAddr.getPort() != port) {
if (domainURL.scheme() != URL_SCHEME_HIFI) {
_sockAddr.clear();
}
if (_domainURL != domainURL || _sockAddr.getPort() != domainURL.port()) {
// re-set the domain info so that auth information is reloaded
hardReset();
if (hostname != _hostname) {
// set the new hostname
_hostname = hostname;
QString previousHost = _domainURL.host();
_domainURL = domainURL;
qCDebug(networking) << "Updated domain hostname to" << _hostname;
if (domainURL.scheme() != URL_SCHEME_HIFI) {
setIsConnected(true);
} else if (previousHost != domainURL.host()) {
qCDebug(networking) << "Updated domain hostname to" << domainURL.host();
// re-set the sock addr to null and fire off a lookup of the IP address for this domain-server's hostname
qCDebug(networking, "Looking up DS hostname %s.", _hostname.toLocal8Bit().constData());
QHostInfo::lookupHost(_hostname, this, SLOT(completedHostnameLookup(const QHostInfo&)));
if (!domainURL.host().isEmpty()) {
// re-set the sock addr to null and fire off a lookup of the IP address for this domain-server's hostname
qCDebug(networking, "Looking up DS hostname %s.", domainURL.host().toLocal8Bit().constData());
QHostInfo::lookupHost(domainURL.host(), this, SLOT(completedHostnameLookup(const QHostInfo&)));
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SetDomainHostname);
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(
LimitedNodeList::ConnectionStep::SetDomainHostname);
UserActivityLogger::getInstance().changedDomain(_hostname);
emit hostnameChanged(_hostname);
UserActivityLogger::getInstance().changedDomain(domainURL.host());
}
}
if (_sockAddr.getPort() != port) {
qCDebug(networking) << "Updated domain port to" << port;
emit domainURLChanged(_domainURL);
if (_sockAddr.getPort() != domainURL.port()) {
qCDebug(networking) << "Updated domain port to" << domainURL.port();
}
// grab the port by reading the string after the colon
_sockAddr.setPort(port);
_sockAddr.setPort(domainURL.port());
}
}
@ -187,10 +199,10 @@ void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname,
if (_iceServerSockAddr.getAddress().toString() != iceServerHostname || id != _pendingDomainID) {
// re-set the domain info to connect to new domain
hardReset();
// refresh our ICE client UUID to something new
_iceClientID = QUuid::createUuid();
_pendingDomainID = id;
HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr;
@ -216,14 +228,18 @@ void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname,
void DomainHandler::activateICELocalSocket() {
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SetDomainSocket);
_sockAddr = _icePeer.getLocalSocket();
_hostname = _sockAddr.getAddress().toString();
_domainURL.setScheme(URL_SCHEME_HIFI);
_domainURL.setHost(_sockAddr.getAddress().toString());
emit domainURLChanged(_domainURL);
emit completedSocketDiscovery();
}
void DomainHandler::activateICEPublicSocket() {
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SetDomainSocket);
_sockAddr = _icePeer.getPublicSocket();
_hostname = _sockAddr.getAddress().toString();
_domainURL.setScheme(URL_SCHEME_HIFI);
_domainURL.setHost(_sockAddr.getAddress().toString());
emit domainURLChanged(_domainURL);
emit completedSocketDiscovery();
}
@ -234,7 +250,7 @@ void DomainHandler::completedHostnameLookup(const QHostInfo& hostInfo) {
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SetDomainSocket);
qCDebug(networking, "DS at %s is at %s", _hostname.toLocal8Bit().constData(),
qCDebug(networking, "DS at %s is at %s", _domainURL.host().toLocal8Bit().constData(),
_sockAddr.getAddress().toString().toLocal8Bit().constData());
emit completedSocketDiscovery();
@ -261,10 +277,12 @@ void DomainHandler::setIsConnected(bool isConnected) {
_isConnected = isConnected;
if (_isConnected) {
emit connectedToDomain(_hostname);
emit connectedToDomain(_domainURL);
// we've connected to new domain - time to ask it for global settings
requestDomainSettings();
if (_domainURL.scheme() == URL_SCHEME_HIFI && !_domainURL.host().isEmpty()) {
// we've connected to new domain - time to ask it for global settings
requestDomainSettings();
}
} else {
emit disconnectedFromDomain();

View file

@ -25,6 +25,7 @@
#include "NLPacketList.h"
#include "Node.h"
#include "ReceivedMessage.h"
#include "NetworkingConstants.h"
const unsigned short DEFAULT_DOMAIN_SERVER_PORT = 40102;
const unsigned short DEFAULT_DOMAIN_SERVER_DTLS_PORT = 40103;
@ -37,14 +38,14 @@ class DomainHandler : public QObject {
Q_OBJECT
public:
DomainHandler(QObject* parent = 0);
void disconnect();
void clearSettings();
const QUuid& getUUID() const { return _uuid; }
void setUUID(const QUuid& uuid);
const QString& getHostname() const { return _hostname; }
QString getHostname() const { return _domainURL.host(); }
const QHostAddress& getIP() const { return _sockAddr.getAddress(); }
void setIPToLocalhost() { _sockAddr.setAddress(QHostAddress(QHostAddress::LocalHost)); }
@ -57,7 +58,7 @@ public:
const QUuid& getConnectionToken() const { return _connectionToken; }
void setConnectionToken(const QUuid& connectionToken) { _connectionToken = connectionToken; }
const QUuid& getAssignmentUUID() const { return _assignmentUUID; }
void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; }
@ -73,11 +74,12 @@ public:
bool isConnected() const { return _isConnected; }
void setIsConnected(bool isConnected);
bool isServerless() const { return _domainURL.scheme() != URL_SCHEME_HIFI; }
bool hasSettings() const { return !_settingsObject.isEmpty(); }
void requestDomainSettings();
const QJsonObject& getSettingsObject() const { return _settingsObject; }
void setPendingPath(const QString& pendingPath) { _pendingPath = pendingPath; }
const QString& getPendingPath() { return _pendingPath; }
void clearPendingPath() { _pendingPath.clear(); }
@ -139,7 +141,7 @@ public:
};
public slots:
void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid());
void setURLAndID(QUrl domainURL, QUuid id);
void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id);
void processSettingsPacketList(QSharedPointer<ReceivedMessage> packetList);
@ -153,14 +155,14 @@ private slots:
void completedIceServerHostnameLookup();
signals:
void hostnameChanged(const QString& hostname);
void domainURLChanged(QUrl domainURL);
// NOTE: the emission of completedSocketDiscovery does not mean a connection to DS is established
// It means that, either from DNS lookup or ICE, we think we have a socket we can talk to DS on
void completedSocketDiscovery();
void resetting();
void connectedToDomain(const QString& hostname);
void connectedToDomain(QUrl domainURL);
void disconnectedFromDomain();
void iceSocketAndIDReceived();
@ -179,7 +181,7 @@ private:
void hardReset();
QUuid _uuid;
QString _hostname;
QUrl _domainURL;
HifiSockAddr _sockAddr;
QUuid _assignmentUUID;
QUuid _connectionToken;
@ -200,4 +202,7 @@ private:
QTimer _apiRefreshTimer;
};
const QString DOMAIN_SPAWNING_POINT { "/0, -10, 0" };
#endif // hifi_DomainHandler_h

View file

@ -17,9 +17,11 @@
#include <StatTracker.h>
#include <shared/FileUtils.h>
#include <PathUtils.h>
#include "NetworkLogging.h"
#include "ResourceManager.h"
#include "NetworkingConstants.h"
void FileResourceRequest::doSend() {
auto statTracker = DependencyManager::get<StatTracker>();
@ -29,7 +31,7 @@ void FileResourceRequest::doSend() {
if (_url.scheme() == URL_SCHEME_QRC) {
filename = ":/" + _url.path();
} else {
filename = _url.toLocalFile();
filename = PathUtils::expandToLocalDataAbsolutePath(_url).toLocalFile();
// sometimes on windows, we see the toLocalFile() return null,
// in this case we will attempt to simply use the url as a string
if (filename.isEmpty()) {

View file

@ -0,0 +1,24 @@
//
// NetworkingConstants.cpp
// libraries/networking/src
//
// Created by Seth Alves on 2018-2-28.
// Copyright 2018 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
//
#include "NetworkingConstants.h"
namespace NetworkingConstants {
// You can change the return of this function if you want to use a custom metaverse URL at compile time
// or you can pass a custom URL via the env variable
QUrl METAVERSE_SERVER_URL() {
const QString HIFI_METAVERSE_URL_ENV = "HIFI_METAVERSE_URL";
const QUrl serverURL = QProcessEnvironment::systemEnvironment().contains(HIFI_METAVERSE_URL_ENV)
? QUrl(QProcessEnvironment::systemEnvironment().value(HIFI_METAVERSE_URL_ENV))
: METAVERSE_SERVER_URL_STABLE;
return serverURL;
};
}

View file

@ -25,18 +25,17 @@ namespace NetworkingConstants {
// if you manually generate a personal access token for the domains scope
// at https://staging.highfidelity.com/user/tokens/new?for_domain_server=true
const QUrl METAVERSE_SERVER_URL_STABLE("https://metaverse.highfidelity.com");
const QUrl METAVERSE_SERVER_URL_STAGING("https://staging.highfidelity.com");
// You can change the return of this function if you want to use a custom metaverse URL at compile time
// or you can pass a custom URL via the env variable
static const QUrl METAVERSE_SERVER_URL() {
static const QString HIFI_METAVERSE_URL_ENV = "HIFI_METAVERSE_URL";
static const QUrl serverURL = QProcessEnvironment::systemEnvironment().contains(HIFI_METAVERSE_URL_ENV)
? QUrl(QProcessEnvironment::systemEnvironment().value(HIFI_METAVERSE_URL_ENV))
: METAVERSE_SERVER_URL_STABLE;
return serverURL;
};
const QUrl METAVERSE_SERVER_URL_STABLE { "https://metaverse.highfidelity.com" };
const QUrl METAVERSE_SERVER_URL_STAGING { "https://staging.highfidelity.com" };
QUrl METAVERSE_SERVER_URL();
}
const QString URL_SCHEME_HIFI = "hifi";
const QString URL_SCHEME_QRC = "qrc";
const QString URL_SCHEME_FILE = "file";
const QString URL_SCHEME_HTTP = "http";
const QString URL_SCHEME_HTTPS = "https";
const QString URL_SCHEME_FTP = "ftp";
const QString URL_SCHEME_ATP = "atp";
#endif // hifi_NetworkingConstants_h

View file

@ -55,7 +55,7 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
// handle domain change signals from AddressManager
connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired,
&_domainHandler, &DomainHandler::setSocketAndID);
&_domainHandler, &DomainHandler::setURLAndID);
connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID,
&_domainHandler, &DomainHandler::setIceServerHostnameAndID);
@ -91,7 +91,7 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
connect(accountManager.data(), &AccountManager::newKeypair, this, &NodeList::sendDomainServerCheckIn);
// clear out NodeList when login is finished
connect(accountManager.data(), SIGNAL(loginComplete()) , this, SLOT(reset()));
connect(accountManager.data(), SIGNAL(loginComplete(const QUrl&)) , this, SLOT(reset()));
// clear our NodeList when logout is requested
connect(accountManager.data(), SIGNAL(logoutComplete()) , this, SLOT(reset()));
@ -106,7 +106,7 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
// setup our timer to send keepalive pings (it's started and stopped on domain connect/disconnect)
_keepAlivePingTimer.setInterval(KEEPALIVE_PING_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable
connect(&_keepAlivePingTimer, &QTimer::timeout, this, &NodeList::sendKeepAlivePings);
connect(&_domainHandler, SIGNAL(connectedToDomain(QString)), &_keepAlivePingTimer, SLOT(start()));
connect(&_domainHandler, SIGNAL(connectedToDomain(QUrl)), &_keepAlivePingTimer, SLOT(start()));
connect(&_domainHandler, &DomainHandler::disconnectedFromDomain, &_keepAlivePingTimer, &QTimer::stop);
// set our sockAddrBelongsToDomainOrNode method as the connection creation filter for the udt::Socket

View file

@ -22,13 +22,6 @@
#include "ResourceRequest.h"
const QString URL_SCHEME_QRC = "qrc";
const QString URL_SCHEME_FILE = "file";
const QString URL_SCHEME_HTTP = "http";
const QString URL_SCHEME_HTTPS = "https";
const QString URL_SCHEME_FTP = "ftp";
const QString URL_SCHEME_ATP = "atp";
class ResourceManager: public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY

View file

@ -1685,6 +1685,15 @@ bool Octree::readFromURL(const QString& urlString) {
}
auto data = request->getData();
QByteArray uncompressedJsonData;
bool wasCompressed = gunzip(data, uncompressedJsonData);
if (wasCompressed) {
QDataStream inputStream(uncompressedJsonData);
return readFromStream(uncompressedJsonData.size(), inputStream, marketplaceID);
}
QDataStream inputStream(data);
return readFromStream(data.size(), inputStream, marketplaceID);
}

View file

@ -81,7 +81,7 @@ const QString& PathUtils::resourcesPath() {
#else
staticResourcePath = ":/";
#endif
#if !defined(Q_OS_ANDROID) && defined(DEV_BUILD)
if (USE_SOURCE_TREE_RESOURCES()) {
// For dev builds, optionally load content from the Git source tree
@ -120,6 +120,31 @@ QUrl PathUtils::resourcesUrl(const QString& relativeUrl) {
return QUrl(resourcesUrl() + relativeUrl);
}
QUrl PathUtils::expandToLocalDataAbsolutePath(const QUrl& fileUrl) {
QString path = fileUrl.path();
if (path.startsWith("/~/")) {
// this results in a qrc:// url...
// return resourcesUrl(path.mid(3));
#ifdef Q_OS_MAC
static const QString staticResourcePath = QCoreApplication::applicationDirPath() + "/../Resources/";
#elif defined (ANDROID)
static const QString staticResourcePath =
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/resources/";
#else
static const QString staticResourcePath = QCoreApplication::applicationDirPath() + "/resources/";
#endif
path.replace(0, 3, staticResourcePath);
QUrl expandedURL = QUrl::fromLocalFile(path);
return expandedURL;
}
QUrl::fromLocalFile(resourcesPath()).toString();
return fileUrl;
}
const QString& PathUtils::qmlBaseUrl() {
static const QString staticResourcePath = resourcesUrl() + "qml/";
return staticResourcePath;

View file

@ -37,6 +37,7 @@ public:
static QUrl resourcesUrl(const QString& relative);
static const QString& resourcesPath();
static const QString& qmlBaseUrl();
static QUrl expandToLocalDataAbsolutePath(const QUrl& fileUrl);
static QUrl qmlUrl(const QString& relative);
#ifdef DEV_BUILD
static const QString& projectRootPath();

View file

@ -16,7 +16,8 @@ var DEFAULT_SCRIPTS_COMBINED = [
"system/+android/touchscreenvirtualpad.js",
"system/+android/bottombar.js",
"system/+android/audio.js" ,
"system/+android/modes.js"/*,
"system/+android/modes.js",
"system/+android/stats.js"/*,
"system/away.js",
"system/controllers/controllerDisplayManager.js",
"system/controllers/handControllerGrabAndroid.js",

View file

@ -0,0 +1,39 @@
"use strict";
//
// stats.js
// scripts/system/
//
// Created by Sam Gondelman on 3/14/18
// Copyright 2018 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
//
(function() { // BEGIN LOCAL_SCOPE
var statsbar;
var statsButton;
function init() {
statsbar = new QmlFragment({
qml: "hifi/StatsBar.qml"
});
statsButton = statsbar.addButton({
icon: "icons/stats.svg",
activeIcon: "icons/stats.svg",
textSize: 45,
bgOpacity: 0.0,
activeBgOpacity: 0.0,
bgColor: "#FFFFFF",
text: "STATS"
});
statsButton.clicked.connect(function() {
Menu.triggerOption("Stats");
});
}
init();
}()); // END LOCAL_SCOPE

View file

@ -698,6 +698,9 @@
Window.location = "hifi://BankOfHighFidelity";
}
break;
case 'wallet_availableUpdatesReceived':
// NOP
break;
default:
print('Unrecognized message from QML:', JSON.stringify(message));
}

View file

@ -777,9 +777,12 @@ function findClickedEntity(event) {
}
var pickRay = Camera.computePickRay(event.x, event.y);
var overlayResult = Overlays.findRayIntersection(pickRay, true, getMainTabletIDs());
if (overlayResult.intersects) {
return null;
var tabletIDs = getMainTabletIDs();
if (tabletIDs.length > 0) {
var overlayResult = Overlays.findRayIntersection(pickRay, true, tabletIDs);
if (overlayResult.intersects) {
return null;
}
}
var entityResult = Entities.findRayIntersection(pickRay, true); // want precision picking
@ -968,8 +971,13 @@ function mouseReleaseEvent(event) {
function wasTabletClicked(event) {
var rayPick = Camera.computePickRay(event.x, event.y);
var result = Overlays.findRayIntersection(rayPick, true, getMainTabletIDs());
return result.intersects;
var tabletIDs = getMainTabletIDs();
if (tabletIDs.length === 0) {
return false;
} else {
var result = Overlays.findRayIntersection(rayPick, true, getMainTabletIDs());
return result.intersects;
}
}
function mouseClickEvent(event) {

View file

@ -30,6 +30,7 @@
var userIsLoggedIn = false;
var walletNeedsSetup = false;
var marketplaceBaseURL = "https://highfidelity.com";
var messagesWaiting = false;
function injectCommonCode(isDirectoryPage) {
@ -205,16 +206,22 @@
purchasesElement.id = "purchasesButton";
purchasesElement.setAttribute('href', "#");
purchasesElement.innerHTML = "My Purchases";
purchasesElement.innerHTML = "";
if (messagesWaiting) {
purchasesElement.innerHTML += "<span style='width:10px;height:10px;background-color:red;border-radius:50%;display:inline-block;'></span> ";
}
purchasesElement.innerHTML += "My Purchases";
// FRONTEND WEBDEV RANT: The username dropdown should REALLY not be programmed to be on the same
// line as the search bar, overlaid on top of the search bar, floated right, and then relatively bumped up using "top:-50px".
$('.navbar-brand').css('margin-right', '10px');
purchasesElement.style = "height:100%;margin-top:18px;font-weight:bold;float:right;margin-right:" + (dropDownElement.offsetWidth + 30) +
"px;position:relative;z-index:999;";
navbarBrandElement.parentNode.insertAdjacentElement('beforeend', purchasesElement);
$('#purchasesButton').on('click', function () {
EventBridge.emitWebEvent(JSON.stringify({
type: "PURCHASES",
referrerURL: window.location.href
referrerURL: window.location.href,
hasUpdates: messagesWaiting
}));
});
}
@ -243,7 +250,7 @@
});
}
function buyButtonClicked(id, name, author, price, href, referrer) {
function buyButtonClicked(id, name, author, price, href, referrer, edition) {
EventBridge.emitWebEvent(JSON.stringify({
type: "CHECKOUT",
itemId: id,
@ -251,7 +258,8 @@
itemPrice: price ? parseInt(price, 10) : 0,
itemHref: href,
referrer: referrer,
itemAuthor: author
itemAuthor: author,
itemEdition: edition
}));
}
@ -319,7 +327,8 @@
$(this).closest('.grid-item').find('.creator').find('.value').text(),
$(this).closest('.grid-item').find('.item-cost').text(),
$(this).attr('data-href'),
"mainPage");
"mainPage",
-1);
});
}
@ -410,7 +419,11 @@
}
var cost = $('.item-cost').text();
if (availability !== 'available') {
var isUpdating = window.location.href.indexOf('edition=') > -1;
var urlParams = new URLSearchParams(window.location.search);
if (isUpdating) {
purchaseButton.html('UPDATE FOR FREE');
} else if (availability !== 'available') {
purchaseButton.html('UNAVAILABLE (' + availability + ')');
} else if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) {
purchaseButton.html('PURCHASE <span class="hifi-glyph hifi-glyph-hfc" style="filter:invert(1);background-size:20px;' +
@ -418,13 +431,14 @@
}
purchaseButton.on('click', function () {
if ('available' === availability) {
if ('available' === availability || isUpdating) {
buyButtonClicked(window.location.pathname.split("/")[3],
$('#top-center').find('h1').text(),
$('#creator').find('.value').text(),
cost,
href,
"itemPage");
"itemPage",
urlParams.get('edition'));
}
});
maybeAddPurchasesButton();
@ -698,6 +712,7 @@
if (marketplaceBaseURL.indexOf('metaverse.') !== -1) {
marketplaceBaseURL = marketplaceBaseURL.replace('metaverse.', '');
}
messagesWaiting = parsedJsonMessage.data.messagesWaiting;
injectCode();
}
}

View file

@ -87,13 +87,24 @@ var selectionDisplay = null; // for gridTool.js to ignore
}
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var NORMAL_ICON = "icons/tablet-icons/market-i.svg";
var NORMAL_ACTIVE = "icons/tablet-icons/market-a.svg";
var WAITING_ICON = "icons/tablet-icons/market-i-msg.svg";
var WAITING_ACTIVE = "icons/tablet-icons/market-a-msg.svg";
var marketplaceButton = tablet.addButton({
icon: "icons/tablet-icons/market-i.svg",
activeIcon: "icons/tablet-icons/market-a.svg",
icon: NORMAL_ICON,
activeIcon: NORMAL_ACTIVE,
text: "MARKET",
sortOrder: 9
});
function messagesWaiting(isWaiting) {
marketplaceButton.editProperties({
icon: (isWaiting ? WAITING_ICON : NORMAL_ICON),
activeIcon: (isWaiting ? WAITING_ACTIVE : NORMAL_ACTIVE)
});
}
function onCanWriteAssetsChanged() {
var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets();
tablet.emitScriptEvent(message);
@ -198,6 +209,7 @@ var selectionDisplay = null; // for gridTool.js to ignore
}
}
var userHasUpdates = false;
function sendCommerceSettings() {
tablet.emitScriptEvent(JSON.stringify({
type: "marketplaces",
@ -206,7 +218,8 @@ var selectionDisplay = null; // for gridTool.js to ignore
commerceMode: Settings.getValue("commerce", true),
userIsLoggedIn: Account.loggedIn,
walletNeedsSetup: Wallet.walletStatus === 1,
metaverseServerURL: Account.metaverseServerURL
metaverseServerURL: Account.metaverseServerURL,
messagesWaiting: userHasUpdates
}
}));
}
@ -583,6 +596,10 @@ var selectionDisplay = null; // for gridTool.js to ignore
case 'purchases_goToMarketplaceClicked':
tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL);
break;
case 'updateItemClicked':
tablet.gotoWebScreen(message.upgradeUrl + "?edition=" + message.itemEdition,
MARKETPLACES_INJECT_SCRIPT_URL);
break;
case 'passphrasePopup_cancelClicked':
case 'needsLogIn_cancelClicked':
tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL);
@ -637,6 +654,11 @@ var selectionDisplay = null; // for gridTool.js to ignore
case 'sendMoney_sendPublicly':
// NOP
break;
case 'wallet_availableUpdatesReceived':
case 'purchases_availableUpdatesReceived':
userHasUpdates = message.numUpdates > 0;
messagesWaiting(userHasUpdates);
break;
default:
print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message));
}

View file

@ -119,7 +119,7 @@ ACClientApp::ACClientApp(int argc, char* argv[]) :
nodeList->startThread();
const DomainHandler& domainHandler = nodeList->getDomainHandler();
connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&)));
connect(&domainHandler, SIGNAL(domainURLChanged(QUrl)), SLOT(domainChanged(QUrl)));
connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ACClientApp::domainConnectionRefused);
connect(nodeList.data(), &NodeList::nodeAdded, this, &ACClientApp::nodeAdded);
@ -169,7 +169,7 @@ void ACClientApp::domainConnectionRefused(const QString& reasonMessage, int reas
qDebug() << "domainConnectionRefused";
}
void ACClientApp::domainChanged(const QString& domainHostname) {
void ACClientApp::domainChanged(QUrl domainURL) {
if (_verbose) {
qDebug() << "domainChanged";
}

View file

@ -29,7 +29,7 @@ public:
private slots:
void domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo);
void domainChanged(const QString& domainHostname);
void domainChanged(QUrl domainURL);
void nodeAdded(SharedNodePointer node);
void nodeActivated(SharedNodePointer node);
void nodeKilled(SharedNodePointer node);

View file

@ -158,7 +158,7 @@ ATPClientApp::ATPClientApp(int argc, char* argv[]) :
nodeList->startThread();
const DomainHandler& domainHandler = nodeList->getDomainHandler();
connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&)));
connect(&domainHandler, SIGNAL(domainURLChanged(QUrl)), SLOT(domainChanged(QUrl)));
connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ATPClientApp::domainConnectionRefused);
connect(nodeList.data(), &NodeList::nodeAdded, this, &ATPClientApp::nodeAdded);
@ -227,7 +227,7 @@ void ATPClientApp::domainConnectionRefused(const QString& reasonMessage, int rea
}
}
void ATPClientApp::domainChanged(const QString& domainHostname) {
void ATPClientApp::domainChanged(QUrl domainURL) {
if (_verbose) {
qDebug() << "domainChanged";
}

View file

@ -31,7 +31,7 @@ public:
private slots:
void domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo);
void domainChanged(const QString& domainHostname);
void domainChanged(QUrl domainURL);
void nodeAdded(SharedNodePointer node);
void nodeActivated(SharedNodePointer node);
void nodeKilled(SharedNodePointer node);

View file

@ -18,6 +18,7 @@
#include "ModelBakingLoggingCategory.h"
#include "BakerCLI.h"
#include "FBXBaker.h"
#include "JSBaker.h"
#include "TextureBaker.h"
BakerCLI::BakerCLI(OvenCLIApplication* parent) : QObject(parent) {
@ -34,6 +35,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString&
qDebug() << "Baking file type: " << type;
static const QString MODEL_EXTENSION { "fbx" };
static const QString SCRIPT_EXTENSION { "js" };
QString extension = type;
@ -44,6 +46,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString&
// check what kind of baker we should be creating
bool isFBX = extension == MODEL_EXTENSION;
bool isScript = extension == SCRIPT_EXTENSION;
bool isSupportedImage = QImageReader::supportedImageFormats().contains(extension.toLatin1());
@ -57,12 +60,16 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString&
outputPath)
};
_baker->moveToThread(Oven::instance().getNextWorkerThread());
} else if (isScript) {
_baker = std::unique_ptr<Baker> { new JSBaker(inputUrl, outputPath) };
_baker->moveToThread(Oven::instance().getNextWorkerThread());
} else if (isSupportedImage) {
_baker = std::unique_ptr<Baker> { new TextureBaker(inputUrl, image::TextureUsage::CUBE_TEXTURE, outputPath) };
_baker->moveToThread(Oven::instance().getNextWorkerThread());
} else {
qCDebug(model_baking) << "Failed to determine baker type for file" << inputUrl;
QCoreApplication::exit(OVEN_STATUS_CODE_FAIL);
return;
}
// invoke the bake method on the baker thread

View file

@ -14,6 +14,7 @@
#include <QtCore/QObject>
#include <QDir>
#include <QUrl>
#include <memory>
@ -31,6 +32,8 @@ class BakerCLI : public QObject {
public:
BakerCLI(OvenCLIApplication* parent);
public slots:
void bakeFile(QUrl inputUrl, const QString& outputPath, const QString& type = QString::null);
private slots:

View file

@ -40,7 +40,8 @@ OvenCLIApplication::OvenCLIApplication(int argc, char* argv[]) :
QUrl inputUrl(QDir::fromNativeSeparators(parser.value(CLI_INPUT_PARAMETER)));
QUrl outputUrl(QDir::fromNativeSeparators(parser.value(CLI_OUTPUT_PARAMETER)));
QString type = parser.isSet(CLI_TYPE_PARAMETER) ? parser.value(CLI_TYPE_PARAMETER) : QString::null;
cli->bakeFile(inputUrl, outputUrl.toString(), type);
QMetaObject::invokeMethod(cli, "bakeFile", Qt::QueuedConnection, Q_ARG(QUrl, inputUrl),
Q_ARG(QString, outputUrl.toString()), Q_ARG(QString, type));
} else {
parser.showHelp();
QCoreApplication::quit();