Merge remote-tracking branch 'gcgithub/android_bubble_fix' into android_experimental_joystick_fixed

This commit is contained in:
Cristian Luis Duarte 2018-03-06 22:02:23 -03:00
commit bde2ba9272
547 changed files with 26221 additions and 11774 deletions

View file

@ -1,12 +1,12 @@
Language: Cpp
Standard: Cpp11
BasedOnStyle: "Chromium"
BasedOnStyle: "Chromium"
ColumnLimit: 128
IndentWidth: 4
UseTab: Never
BreakBeforeBraces: Custom
BraceWrapping:
BraceWrapping:
AfterEnum: true
AfterClass: false
AfterControlStatement: false
@ -21,11 +21,11 @@ BraceWrapping:
AccessModifierOffset: -4
AllowShortFunctionsOnASingleLine: InlineOnly
BreakConstructorInitializers: BeforeColon
BreakConstructorInitializersBeforeComma: true
AllowShortFunctionsOnASingleLine: InlineOnly
BreakConstructorInitializers: AfterColon
BreakConstructorInitializersBeforeComma: false
IndentCaseLabels: true
ReflowComments: false
ReflowComments: false
Cpp11BracedListStyle: false
ContinuationIndentWidth: 4
ConstructorInitializerAllOnOneLineOrOnePerLine: false

View file

@ -1,6 +1,6 @@
set(TARGET_NAME native-lib)
setup_hifi_library()
link_hifi_libraries(shared networking gl gpu image fbx render-utils physics entities octree ${PLATFORM_GL_BACKEND})
link_hifi_libraries(shared task networking gl gpu qml image fbx render-utils physics entities octree ${PLATFORM_GL_BACKEND})
target_opengl()
target_bullet()

View file

@ -44,7 +44,6 @@
android:label="@string/app_name"
android:screenOrientation="landscape"
android:launchMode="singleTop"
android:enableVrMode="com.google.vr.vrcore/com.google.vr.vrcore.common.VrCoreListenerService"
>
<intent-filter>

View file

@ -340,7 +340,6 @@ void Agent::scriptRequestFinished() {
request->deleteLater();
}
void Agent::executeScript() {
_scriptEngine = scriptEngineFactory(ScriptEngine::AGENT_SCRIPT, _scriptContents, _payload);

View file

@ -257,12 +257,10 @@ AssetServer::AssetServer(ReceivedMessage& message) :
_transferTaskPool.setMaxThreadCount(TASK_POOL_THREAD_COUNT);
_bakingTaskPool.setMaxThreadCount(1);
// Queue all requests until the Asset Server is fully setup
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet");
packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo");
packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload");
packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation");
packetReceiver.registerListenerForTypes({ PacketType::AssetGet, PacketType::AssetGetInfo, PacketType::AssetUpload, PacketType::AssetMappingOperation }, this, "queueRequests");
#ifdef Q_OS_WIN
updateConsumedCores();
QTimer* timer = new QTimer(this);
@ -291,6 +289,7 @@ void AssetServer::aboutToFinish() {
if (pendingRunnable) {
it = _pendingBakes.erase(it);
} else {
qDebug() << "Aborting bake for" << it.key();
it.value()->abort();
++it;
}
@ -396,6 +395,7 @@ void AssetServer::completeSetup() {
if (_fileMappings.size() > 0) {
cleanupUnmappedFiles();
cleanupBakedFilesForDeletedAssets();
}
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
@ -417,10 +417,65 @@ void AssetServer::completeSetup() {
PathUtils::removeTemporaryApplicationDirs();
PathUtils::removeTemporaryApplicationDirs("Oven");
qCDebug(asset_server) << "Overriding temporary queuing packet handler.";
// We're fully setup, override the request queueing handler and replay all requests
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet");
packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo");
packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload");
packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation");
replayRequests();
}
void AssetServer::queueRequests(QSharedPointer<ReceivedMessage> packet, SharedNodePointer senderNode) {
qCDebug(asset_server) << "Queuing requests until fully setup";
QMutexLocker lock { &_queuedRequestsMutex };
_queuedRequests.push_back({ packet, senderNode });
// If we've stopped queueing but the callback was already in flight,
// then replay it immediately.
if (!_isQueueingRequests) {
lock.unlock();
replayRequests();
}
}
void AssetServer::replayRequests() {
RequestQueue queue;
{
QMutexLocker lock { &_queuedRequestsMutex };
std::swap(queue, _queuedRequests);
_isQueueingRequests = false;
}
qCDebug(asset_server) << "Replaying" << queue.size() << "requests.";
for (const auto& request : queue) {
switch (request.first->getType()) {
case PacketType::AssetGet:
handleAssetGet(request.first, request.second);
break;
case PacketType::AssetGetInfo:
handleAssetGetInfo(request.first, request.second);
break;
case PacketType::AssetUpload:
handleAssetUpload(request.first, request.second);
break;
case PacketType::AssetMappingOperation:
handleAssetMappingOperation(request.first, request.second);
break;
default:
qCWarning(asset_server) << "Unknown queued request type:" << request.first->getType();
break;
}
}
}
void AssetServer::cleanupUnmappedFiles() {
QRegExp hashFileRegex { "^[a-f0-9]{" + QString::number(AssetUtils::SHA256_HASH_HEX_LENGTH) + "}" };
QRegExp hashFileRegex { AssetUtils::ASSET_HASH_REGEX_STRING };
auto files = _filesDirectory.entryInfoList(QDir::Files);
@ -452,6 +507,38 @@ void AssetServer::cleanupUnmappedFiles() {
}
}
void AssetServer::cleanupBakedFilesForDeletedAssets() {
qCInfo(asset_server) << "Performing baked asset cleanup for deleted assets";
std::set<AssetUtils::AssetHash> bakedHashes;
for (const auto& it : _fileMappings) {
// check if this is a mapping to baked content
if (it.first.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) {
// extract the hash from the baked mapping
AssetUtils::AssetHash hash = it.first.mid(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER.length(),
AssetUtils::SHA256_HASH_HEX_LENGTH);
// add the hash to our set of hashes for which we have baked content
bakedHashes.insert(hash);
}
}
// enumerate the hashes for which we have baked content
for (const auto& hash : bakedHashes) {
// check if we have a mapping that points to this hash
auto matchingMapping = std::find_if(std::begin(_fileMappings), std::end(_fileMappings),
[&hash](const std::pair<AssetUtils::AssetPath, AssetUtils::AssetHash> mappingPair) {
return mappingPair.second == hash;
});
if (matchingMapping == std::end(_fileMappings)) {
// we didn't find a mapping for this hash, remove any baked content we still have for it
removeBakedPathsForDeletedAsset(hash);
}
}
}
void AssetServer::handleAssetMappingOperation(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
using AssetMappingOperationType = AssetUtils::AssetMappingOperationType;
@ -464,32 +551,41 @@ void AssetServer::handleAssetMappingOperation(QSharedPointer<ReceivedMessage> me
auto replyPacket = NLPacketList::create(PacketType::AssetMappingOperationReply, QByteArray(), true, true);
replyPacket->writePrimitive(messageID);
bool canWriteToAssetServer = true;
if (senderNode) {
canWriteToAssetServer = senderNode->getCanWriteToAssetServer();
}
switch (operationType) {
case AssetMappingOperationType::Get:
handleGetMappingOperation(*message, senderNode, *replyPacket);
handleGetMappingOperation(*message, *replyPacket);
break;
case AssetMappingOperationType::GetAll:
handleGetAllMappingOperation(*message, senderNode, *replyPacket);
handleGetAllMappingOperation(*replyPacket);
break;
case AssetMappingOperationType::Set:
handleSetMappingOperation(*message, senderNode, *replyPacket);
handleSetMappingOperation(*message, canWriteToAssetServer, *replyPacket);
break;
case AssetMappingOperationType::Delete:
handleDeleteMappingsOperation(*message, senderNode, *replyPacket);
handleDeleteMappingsOperation(*message, canWriteToAssetServer, *replyPacket);
break;
case AssetMappingOperationType::Rename:
handleRenameMappingOperation(*message, senderNode, *replyPacket);
handleRenameMappingOperation(*message, canWriteToAssetServer, *replyPacket);
break;
case AssetMappingOperationType::SetBakingEnabled:
handleSetBakingEnabledOperation(*message, senderNode, *replyPacket);
handleSetBakingEnabledOperation(*message, canWriteToAssetServer, *replyPacket);
break;
}
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacketList(std::move(replyPacket), *senderNode);
if (senderNode) {
nodeList->sendPacketList(std::move(replyPacket), *senderNode);
} else {
nodeList->sendPacketList(std::move(replyPacket), message->getSenderSockAddr());
}
}
void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketList& replyPacket) {
QString assetPath = message.readString();
QUrl url { assetPath };
@ -568,7 +664,7 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNode
}
}
void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
void AssetServer::handleGetAllMappingOperation(NLPacketList& replyPacket) {
replyPacket.writePrimitive(AssetUtils::AssetServerError::NoError);
uint32_t count = (uint32_t)_fileMappings.size();
@ -591,8 +687,8 @@ void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedN
}
}
void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanWriteToAssetServer()) {
void AssetServer::handleSetMappingOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket) {
if (hasWriteAccess) {
QString assetPath = message.readString();
auto assetHash = message.read(AssetUtils::SHA256_HASH_LENGTH).toHex();
@ -614,8 +710,8 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode
}
}
void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanWriteToAssetServer()) {
void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket) {
if (hasWriteAccess) {
int numberOfDeletedMappings { 0 };
message.readPrimitive(&numberOfDeletedMappings);
@ -642,8 +738,8 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared
}
}
void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanWriteToAssetServer()) {
void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket) {
if (hasWriteAccess) {
QString oldPath = message.readString();
QString newPath = message.readString();
@ -664,8 +760,8 @@ void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedN
}
}
void AssetServer::handleSetBakingEnabledOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanWriteToAssetServer()) {
void AssetServer::handleSetBakingEnabledOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket) {
if (hasWriteAccess) {
bool enabled { true };
message.readPrimitive(&enabled);
@ -739,9 +835,14 @@ void AssetServer::handleAssetGet(QSharedPointer<ReceivedMessage> message, Shared
}
void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
bool canWriteToAssetServer = true;
if (senderNode) {
canWriteToAssetServer = senderNode->getCanWriteToAssetServer();
}
if (senderNode->getCanWriteToAssetServer()) {
qCDebug(asset_server) << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID());
if (canWriteToAssetServer) {
qCDebug(asset_server) << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(message->getSourceID());
auto task = new UploadAssetTask(message, senderNode, _filesDirectory, _filesizeLimit);
_transferTaskPool.start(task);
@ -761,7 +862,11 @@ void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, Sha
// send off the packet
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacket(std::move(permissionErrorPacket), *senderNode);
if (senderNode) {
nodeList->sendPacket(std::move(permissionErrorPacket), *senderNode);
} else {
nodeList->sendPacket(std::move(permissionErrorPacket), message->getSenderSockAddr());
}
}
}
@ -1283,6 +1388,8 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina
}
void AssetServer::handleAbortedBake(QString originalAssetHash, QString assetPath) {
qDebug() << "Aborted bake:" << originalAssetHash;
// for an aborted bake we don't do anything but remove the BakeAssetTask from our pending bakes
_pendingBakes.remove(originalAssetHash);
}

View file

@ -21,17 +21,9 @@
#include "AssetUtils.h"
#include "ReceivedMessage.h"
namespace std {
template <>
struct hash<QString> {
size_t operator()(const QString& v) const { return qHash(v); }
};
}
#include "RegisteredMetaTypes.h"
struct AssetMeta {
AssetMeta() {
}
int bakeVersion { 0 };
bool failedLastBake { false };
QString lastBakeErrors;
@ -52,6 +44,7 @@ public slots:
private slots:
void completeSetup();
void queueRequests(QSharedPointer<ReceivedMessage> packet, SharedNodePointer senderNode);
void handleAssetGetInfo(QSharedPointer<ReceivedMessage> packet, SharedNodePointer senderNode);
void handleAssetGet(QSharedPointer<ReceivedMessage> packet, SharedNodePointer senderNode);
void handleAssetUpload(QSharedPointer<ReceivedMessage> packetList, SharedNodePointer senderNode);
@ -60,14 +53,17 @@ private slots:
void sendStatsPacket() override;
private:
using Mappings = std::unordered_map<QString, QString>;
void replayRequests();
void handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleSetBakingEnabledOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleGetMappingOperation(ReceivedMessage& message, NLPacketList& replyPacket);
void handleGetAllMappingOperation(NLPacketList& replyPacket);
void handleSetMappingOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket);
void handleDeleteMappingsOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket);
void handleRenameMappingOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket);
void handleSetBakingEnabledOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket);
void handleAssetServerBackup(ReceivedMessage& message, NLPacketList& replyPacket);
void handleAssetServerRestore(ReceivedMessage& message, NLPacketList& replyPacket);
// Mapping file operations must be called from main assignment thread only
bool loadMappingsFromFile();
@ -87,6 +83,9 @@ private:
/// Delete any unmapped files from the local asset directory
void cleanupUnmappedFiles();
/// Delete any baked files for assets removed from the local asset directory
void cleanupBakedFilesForDeletedAssets();
QString getPathToAssetHash(const AssetUtils::AssetHash& assetHash);
std::pair<AssetUtils::BakingStatus, QString> getAssetStatus(const AssetUtils::AssetPath& path, const AssetUtils::AssetHash& hash);
@ -111,7 +110,7 @@ private:
/// Remove baked paths when the original asset is deleteds
void removeBakedPathsForDeletedAsset(AssetUtils::AssetHash originalAssetHash);
Mappings _fileMappings;
AssetUtils::Mappings _fileMappings;
QDir _resourcesDirectory;
QDir _filesDirectory;
@ -122,6 +121,11 @@ private:
QHash<AssetUtils::AssetHash, std::shared_ptr<BakeAssetTask>> _pendingBakes;
QThreadPool _bakingTaskPool;
QMutex _queuedRequestsMutex;
bool _isQueueingRequests { true };
using RequestQueue = QVector<QPair<QSharedPointer<ReceivedMessage>, SharedNodePointer>>;
RequestQueue _queuedRequests;
bool _wasColorTextureCompressionEnabled { false };
bool _wasGrayscaleTextureCompressionEnabled { false };
bool _wasNormalTextureCompressionEnabled { false };

View file

@ -69,8 +69,10 @@ void BakeAssetTask::run() {
_ovenProcess.reset(new QProcess());
QEventLoop loop;
connect(_ovenProcess.get(), static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
this, [this, tempOutputDir](int exitCode, QProcess::ExitStatus exitStatus) {
this, [&loop, this, tempOutputDir](int exitCode, QProcess::ExitStatus exitStatus) {
qDebug() << "Baking process finished: " << exitCode << exitStatus;
if (exitStatus == QProcess::CrashExit) {
@ -108,6 +110,7 @@ void BakeAssetTask::run() {
emit bakeFailed(_assetHash, _assetPath, errors);
}
loop.quit();
});
qDebug() << "Starting oven for " << _assetPath;
@ -117,11 +120,21 @@ void BakeAssetTask::run() {
emit bakeFailed(_assetHash, _assetPath, errors);
return;
}
_ovenProcess->waitForFinished();
_isBaking = true;
loop.exec();
}
void BakeAssetTask::abort() {
if (!_wasAborted.exchange(true)) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "abort");
return;
}
qDebug() << "Aborting BakeAssetTask for" << _assetHash;
if (_ovenProcess->state() != QProcess::NotRunning) {
qDebug() << "Teminating oven process for" << _assetHash;
_wasAborted = true;
_ovenProcess->terminate();
}
}

View file

@ -27,12 +27,14 @@ class BakeAssetTask : public QObject, public QRunnable {
public:
BakeAssetTask(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath);
// Thread-safe inspection methods
bool isBaking() { return _isBaking.load(); }
bool wasAborted() const { return _wasAborted.load(); }
void run() override;
public slots:
void abort();
bool wasAborted() const { return _wasAborted.load(); }
signals:
void bakeComplete(QString assetHash, QString assetPath, QString tempOutputDir, QVector<QString> outputFiles);

View file

@ -112,5 +112,9 @@ void SendAssetTask::run() {
}
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacketList(std::move(replyPacketList), *_senderNode);
if (_senderNode) {
nodeList->sendPacketList(std::move(replyPacketList), *_senderNode);
} else {
nodeList->sendPacketList(std::move(replyPacketList), _message->getSenderSockAddr());
}
}

View file

@ -41,9 +41,12 @@ void UploadAssetTask::run() {
uint64_t fileSize;
buffer.read(reinterpret_cast<char*>(&fileSize), sizeof(fileSize));
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes from"
<< uuidStringWithoutCurlyBraces(_senderNode->getUUID());
if (_senderNode) {
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID());
} else {
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes from" << _receivedMessage->getSenderSockAddr();
}
auto replyPacket = NLPacket::create(PacketType::AssetUploadReply, -1, true);
replyPacket->writePrimitive(messageID);
@ -55,9 +58,12 @@ void UploadAssetTask::run() {
auto hash = AssetUtils::hashData(fileData);
auto hexHash = hash.toHex();
qDebug() << "Hash for uploaded file from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID())
<< "is: (" << hexHash << ") ";
if (_senderNode) {
qDebug() << "Hash for uploaded file from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID()) << "is: (" << hexHash << ")";
} else {
qDebug() << "Hash for uploaded file from" << _receivedMessage->getSenderSockAddr() << "is: (" << hexHash << ")";
}
QFile file { _resourcesDir.filePath(QString(hexHash)) };
@ -103,5 +109,9 @@ void UploadAssetTask::run() {
}
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacket(std::move(replyPacket), *_senderNode);
if (_senderNode) {
nodeList->sendPacket(std::move(replyPacket), *_senderNode);
} else {
nodeList->sendPacket(std::move(replyPacket), _receivedMessage->getSenderSockAddr());
}
}

View file

@ -9,6 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AudioMixer.h"
#include <thread>
#include <QtCore/QJsonArray>
@ -36,8 +38,6 @@
#include "AvatarAudioStream.h"
#include "InjectedAudioStream.h"
#include "AudioMixer.h"
static const float DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE = 0.5f; // attenuation = -6dB * log2(distance)
static const int DISABLE_STATIC_JITTER_FRAMES = -1;
static const float DEFAULT_NOISE_MUTING_THRESHOLD = 1.0f;

View file

@ -18,6 +18,8 @@
#include <ThreadedAssignment.h>
#include <UUIDHasher.h>
#include <plugins/Forward.h>
#include "AudioMixerStats.h"
#include "AudioMixerSlavePool.h"

View file

@ -9,6 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AudioMixerClientData.h"
#include <random>
#include <QtCore/QDebug>
@ -22,8 +24,6 @@
#include "AudioLogging.h"
#include "AudioHelpers.h"
#include "AudioMixer.h"
#include "AudioMixerClientData.h"
AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) :
NodeData(nodeID),

View file

@ -21,6 +21,7 @@
#include <AudioLimiter.h>
#include <UUIDHasher.h>
#include <plugins/Forward.h>
#include <plugins/CodecPlugin.h>
#include "PositionalAudioStream.h"

View file

@ -9,6 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AudioMixerSlave.h"
#include <algorithm>
#include <glm/glm.hpp>
@ -34,8 +36,6 @@
#include "InjectedAudioStream.h"
#include "AudioHelpers.h"
#include "AudioMixerSlave.h"
using AudioStreamMap = AudioMixerClientData::AudioStreamMap;
// packet helpers
@ -49,7 +49,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData&
inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
const glm::vec3& relativePosition);
inline float computeGain(const AudioMixerClientData& listenerNodeData, const AvatarAudioStream& listeningNodeStream,
const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, bool isEcho);
const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho);
inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
const glm::vec3& relativePosition);
@ -176,7 +176,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) {
auto nodeID = node->getUUID();
// compute the node's max relative volume
float nodeVolume;
float nodeVolume = 0.0f;
for (auto& streamPair : nodeData->getAudioStreams()) {
auto nodeStream = streamPair.second;
@ -193,10 +193,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) {
}
// max-heapify the nodes by relative volume
throttledNodes.push_back(std::make_pair(nodeVolume, node));
if (!throttledNodes.empty()) {
std::push_heap(throttledNodes.begin(), throttledNodes.end());
}
throttledNodes.push_back({ nodeVolume, node });
std::push_heap(throttledNodes.begin(), throttledNodes.end());
}
}
});
@ -278,7 +276,7 @@ void AudioMixerSlave::addStream(AudioMixerClientData& listenerNodeData, const QU
glm::vec3 relativePosition = streamToAdd.getPosition() - listeningNodeStream.getPosition();
float distance = glm::max(glm::length(relativePosition), EPSILON);
float gain = computeGain(listenerNodeData, listeningNodeStream, streamToAdd, relativePosition, isEcho);
float gain = computeGain(listenerNodeData, listeningNodeStream, streamToAdd, relativePosition, distance, isEcho);
float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition);
const int HRTF_DATASET_INDEX = 1;
@ -491,9 +489,6 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi
// avatar: skip attenuation - it is too costly to approximate
// distance attenuation: approximate, ignore zone-specific attenuations
// this is a good approximation for streams further than ATTENUATION_START_DISTANCE
// those streams closer will be amplified; amplifying close streams is acceptable
// when throttling, as close streams are expected to be heard by a user
float distance = glm::length(relativePosition);
return gain / distance;
@ -501,7 +496,7 @@ float approximateGain(const AvatarAudioStream& listeningNodeStream, const Positi
}
float computeGain(const AudioMixerClientData& listenerNodeData, const AvatarAudioStream& listeningNodeStream,
const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, bool isEcho) {
const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho) {
float gain = 1.0f;
// injector: apply attenuation
@ -538,23 +533,13 @@ float computeGain(const AudioMixerClientData& listenerNodeData, const AvatarAudi
break;
}
}
// translate the zone setting to gain per log2(distance)
float g = glm::clamp(1.0f - attenuationPerDoublingInDistance, EPSILON, 1.0f);
// distance attenuation
const float ATTENUATION_START_DISTANCE = 1.0f;
float distance = glm::length(relativePosition);
assert(ATTENUATION_START_DISTANCE > EPSILON);
if (distance >= ATTENUATION_START_DISTANCE) {
// translate the zone setting to gain per log2(distance)
float g = 1.0f - attenuationPerDoublingInDistance;
g = glm::clamp(g, EPSILON, 1.0f);
// calculate the distance coefficient using the distance to this node
float distanceCoefficient = fastExp2f(fastLog2f(g) * fastLog2f(distance/ATTENUATION_START_DISTANCE));
// multiply the current attenuation coefficient by the distance coefficient
gain *= distanceCoefficient;
}
// calculate the attenuation using the distance to this node
// reference attenuation of 0dB at distance = 1.0m
gain *= fastExp2f(fastLog2f(g) * fastLog2f(std::max(distance, HRTF_NEARFIELD_MIN)));
gain = std::min(gain, 1.0f / HRTF_NEARFIELD_MIN);
return gain;
}

View file

@ -116,7 +116,6 @@ void EntityServer::beforeRun() {
void EntityServer::entityCreated(const EntityItem& newEntity, const SharedNodePointer& senderNode) {
}
// EntityServer will use the "special packets" to send list of recently deleted entities
bool EntityServer::hasSpecialPacketsToSend(const SharedNodePointer& node) {
bool shouldSendDeletedEntities = false;
@ -277,7 +276,6 @@ int EntityServer::sendSpecialPackets(const SharedNodePointer& node, OctreeQueryN
return totalBytes;
}
void EntityServer::pruneDeletedEntities() {
EntityTreePointer tree = std::static_pointer_cast<EntityTree>(_tree);
if (tree->hasAnyDeletedEntities()) {

View file

@ -30,7 +30,6 @@ struct ViewerSendingStats {
class SimpleEntitySimulation;
using SimpleEntitySimulationPointer = std::shared_ptr<SimpleEntitySimulation>;
class EntityServer : public OctreeServer, public NewlyCreatedEntityHook {
Q_OBJECT
public:
@ -38,7 +37,7 @@ public:
~EntityServer();
// Subclasses must implement these methods
virtual std::unique_ptr<OctreeQueryNode> createOctreeQueryNode() override ;
virtual std::unique_ptr<OctreeQueryNode> createOctreeQueryNode() override;
virtual char getMyNodeType() const override { return NodeType::EntityServer; }
virtual PacketType getMyQueryMessageType() const override { return PacketType::EntityQuery; }
virtual const char* getMyServerName() const override { return MODEL_SERVER_NAME; }
@ -82,12 +81,12 @@ private:
QReadWriteLock _viewerSendingStatsLock;
QMap<QUuid, QMap<QUuid, ViewerSendingStats>> _viewerSendingStats;
static const int DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 45 * 60 * 1000; // 45m
static const int DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 60 * 60 * 1000; // 1h
int _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 45m
int _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 1h
static const int DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 45 * 60 * 1000; // 45m
static const int DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 60 * 60 * 1000; // 1h
int _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 45m
int _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 1h
QTimer _dynamicDomainVerificationTimer;
void startDynamicDomainVerification();
};
#endif // hifi_EntityServer_h
#endif // hifi_EntityServer_h

View file

@ -9,22 +9,13 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <LogHandler.h>
#include <BuildInfo.h>
#include <SharedUtil.h>
#include "AssignmentClientApp.h"
#include <BuildInfo.h>
int main(int argc, char* argv[]) {
disableQtBearerPoll(); // Fixes wifi ping spikes
QCoreApplication::setApplicationName(BuildInfo::ASSIGNMENT_CLIENT_NAME);
QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION);
QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN);
QCoreApplication::setApplicationVersion(BuildInfo::VERSION);
qInstallMessageHandler(LogHandler::verboseMessageHandler);
qInfo() << "Starting.";
setupHifiApplication(BuildInfo::ASSIGNMENT_CLIENT_NAME);
AssignmentClientApp app(argc, argv);

View file

@ -33,6 +33,10 @@
#include <PathUtils.h>
#include <QtCore/QDir>
#include <OctreeDataUtils.h>
Q_LOGGING_CATEGORY(octree_server, "hifi.octree-server")
int OctreeServer::_clientCount = 0;
const int MOVING_AVERAGE_SAMPLE_COUNTS = 1000;
@ -84,6 +88,8 @@ int OctreeServer::_longProcessWait = 0;
int OctreeServer::_shortProcessWait = 0;
int OctreeServer::_noProcessWait = 0;
static const QString PERSIST_FILE_DOWNLOAD_PATH = "/models.json.gz";
void OctreeServer::resetSendingStats() {
_averageLoopTime.reset();
@ -202,7 +208,6 @@ void OctreeServer::trackPacketSendingTime(float time) {
}
}
void OctreeServer::trackProcessWaitTime(float time) {
const float MAX_SHORT_TIME = 10.0f;
const float MAX_LONG_TIME = 100.0f;
@ -283,8 +288,6 @@ void OctreeServer::initHTTPManager(int port) {
_httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this);
}
const QString PERSIST_FILE_DOWNLOAD_PATH = "/models.json.gz";
bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) {
#ifdef FORCE_CRASH
@ -922,87 +925,6 @@ void OctreeServer::handleOctreeDataNackPacket(QSharedPointer<ReceivedMessage> me
}
}
void OctreeServer::handleOctreeFileReplacement(QSharedPointer<ReceivedMessage> message) {
if (!_isFinished && !_isShuttingDown) {
// these messages are only allowed to come from the domain server, so make sure that is the case
auto nodeList = DependencyManager::get<NodeList>();
if (message->getSenderSockAddr() == nodeList->getDomainHandler().getSockAddr()) {
// it's far cleaner to load up the new content upon server startup
// so here we just store a special file at our persist path
// and then force a stop of the server so that it can pick it up when it relaunches
if (!_persistAbsoluteFilePath.isEmpty()) {
replaceContentFromMessageData(message->getMessage());
} else {
qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known";
}
} else {
qDebug() << "Received an octree file replacement that was not from our domain server - refusing to process";
}
}
}
// Message->getMessage() contains a QByteArray representation of the URL to download from
void OctreeServer::handleOctreeFileReplacementFromURL(QSharedPointer<ReceivedMessage> message) {
qInfo() << "Received request to replace content from a url";
if (!_isFinished && !_isShuttingDown) {
// This call comes from Interface, so we skip our domain server check
// but confirm that we have permissions to replace content sets
if (DependencyManager::get<NodeList>()->getThisNodeCanReplaceContent()) {
if (!_persistAbsoluteFilePath.isEmpty()) {
// Convert message data into our URL
QString url(message->getMessage());
QUrl modelsURL = QUrl(url, QUrl::StrictMode);
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest request(modelsURL);
QNetworkReply* reply = networkAccessManager.get(request);
connect(reply, &QNetworkReply::finished, [this, reply, modelsURL]() {
QNetworkReply::NetworkError networkError = reply->error();
if (networkError == QNetworkReply::NoError) {
QByteArray contents = reply->readAll();
replaceContentFromMessageData(contents);
} else {
qDebug() << "Error downloading JSON from specified file";
}
});
} else {
qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known";
}
}
}
}
void OctreeServer::replaceContentFromMessageData(QByteArray content) {
//Assume we have compressed data
auto compressedOctree = content;
QByteArray jsonOctree;
bool wasCompressed = gunzip(compressedOctree, jsonOctree);
if (!wasCompressed) {
// the source was not compressed, assume we were sent regular JSON data
jsonOctree = compressedOctree;
}
// check the JSON data to verify it is an object
if (QJsonDocument::fromJson(jsonOctree).isObject()) {
if (!wasCompressed) {
// source was not compressed, we compress it before we write it locally
gzip(jsonOctree, compressedOctree);
}
// write the compressed octree data to a special file
auto replacementFilePath = _persistAbsoluteFilePath.append(OctreePersistThread::REPLACEMENT_FILE_EXTENSION);
QFile replacementFile(replacementFilePath);
if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) {
// we've now written our replacement file, time to take the server down so it can
// process it when it comes back up
qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server";
setFinished(true);
} else {
qWarning() << "Could not write replacement octree data to file - refusing to process";
}
} else {
qDebug() << "Received replacement octree file that is invalid - refusing to process";
}
}
bool OctreeServer::readOptionBool(const QString& optionName, const QJsonObject& settingsSectionObject, bool& result) {
result = false; // assume it doesn't exist
bool optionAvailable = false;
@ -1119,7 +1041,18 @@ void OctreeServer::readConfiguration() {
_persistFilePath = getMyDefaultPersistFilename();
}
QDir persistPath { _persistFilePath };
if (persistPath.isRelative()) {
// if the domain settings passed us a relative path, make an absolute path that is relative to the
// default data directory
_persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath);
} else {
_persistAbsoluteFilePath = persistPath.absolutePath();
}
qDebug() << "persistFilePath=" << _persistFilePath;
qDebug() << "persisAbsoluteFilePath=" << _persistAbsoluteFilePath;
_persistAsFileType = "json.gz";
@ -1200,20 +1133,94 @@ void OctreeServer::run() {
}
void OctreeServer::domainSettingsRequestComplete() {
if (_state != OctreeServerState::WaitingForDomainSettings) {
qCWarning(octree_server) << "Received domain settings after they have already been received";
return;
}
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket");
packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket");
packetReceiver.registerListener(PacketType::OctreeDataFileReply, this, "handleOctreeDataFileReply");
qDebug(octree_server) << "Received domain settings";
readConfiguration();
_state = OctreeServerState::WaitingForOctreeDataNegotation;
auto nodeList = DependencyManager::get<NodeList>();
const DomainHandler& domainHandler = nodeList->getDomainHandler();
auto packet = NLPacket::create(PacketType::OctreeDataFileRequest, -1, true, false);
OctreeUtils::RawOctreeData data;
qCDebug(octree_server) << "Reading octree data from" << _persistAbsoluteFilePath;
if (data.readOctreeDataInfoFromFile(_persistAbsoluteFilePath)) {
qCDebug(octree_server) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.version << ")";
packet->writePrimitive(true);
auto id = data.id.toRfc4122();
packet->write(id);
packet->writePrimitive(data.version);
} else {
qCWarning(octree_server) << "No octree data found";
packet->writePrimitive(false);
}
qCDebug(octree_server) << "Sending request for octree data to DS";
nodeList->sendPacket(std::move(packet), domainHandler.getSockAddr());
}
void OctreeServer::handleOctreeDataFileReply(QSharedPointer<ReceivedMessage> message) {
if (_state != OctreeServerState::WaitingForOctreeDataNegotation) {
qCWarning(octree_server) << "Server received ocree data file reply but is not currently negotiating.";
return;
}
bool includesNewData;
message->readPrimitive(&includesNewData);
QByteArray replaceData;
if (includesNewData) {
replaceData = message->readAll();
qDebug() << "Got reply to octree data file request, new data sent";
} else {
qDebug() << "Got reply to octree data file request, current entity data is sufficient";
OctreeUtils::RawEntityData data;
qCDebug(octree_server) << "Reading octree data from" << _persistAbsoluteFilePath;
if (data.readOctreeDataInfoFromFile(_persistAbsoluteFilePath)) {
if (data.id.isNull()) {
qCDebug(octree_server) << "Current octree data has a null id, updating";
data.resetIdAndVersion();
QFile file(_persistAbsoluteFilePath);
if (file.open(QIODevice::WriteOnly)) {
auto entityData = data.toGzippedByteArray();
file.write(entityData);
file.close();
} else {
qCDebug(octree_server) << "Failed to update octree data";
}
}
}
}
_state = OctreeServerState::Running;
beginRunning(replaceData);
}
void OctreeServer::beginRunning(QByteArray replaceData) {
if (_state != OctreeServerState::Running) {
qCWarning(octree_server) << "Server is not running";
return;
}
auto nodeList = DependencyManager::get<NodeList>();
// we need to ask the DS about agents so we can ping/reply with them
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket");
packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket");
packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacement");
packetReceiver.registerListener(PacketType::OctreeFileReplacementFromUrl, this, "handleOctreeFileReplacementFromURL");
readConfiguration();
beforeRun(); // after payload has been processed
connect(nodeList.data(), SIGNAL(nodeAdded(SharedNodePointer)), SLOT(nodeAdded(SharedNodePointer)));
@ -1233,17 +1240,6 @@ void OctreeServer::domainSettingsRequestComplete() {
// if we want Persistence, set up the local file and persist thread
if (_wantPersist) {
// If persist filename does not exist, let's see if there is one beside the application binary
// If there is, let's copy it over to our target persist directory
QDir persistPath { _persistFilePath };
_persistAbsoluteFilePath = persistPath.absolutePath();
if (persistPath.isRelative()) {
// if the domain settings passed us a relative path, make an absolute path that is relative to the
// default data directory
_persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath);
}
static const QString ENTITY_PERSIST_EXTENSION = ".json.gz";
// force the persist file to end with .json.gz
@ -1328,7 +1324,7 @@ void OctreeServer::domainSettingsRequestComplete() {
// now set up PersistThread
_persistThread = new OctreePersistThread(_tree, _persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval,
_wantBackup, _settings, _debugTimestampNow, _persistAsFileType);
_wantBackup, _settings, _debugTimestampNow, _persistAsFileType, replaceData);
_persistThread->initialize(true);
}

View file

@ -27,8 +27,18 @@
#include "OctreeServerConsts.h"
#include "OctreeInboundPacketProcessor.h"
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(octree_server)
const int DEFAULT_PACKETS_PER_INTERVAL = 2000; // some 120,000 packets per second total
enum class OctreeServerState {
WaitingForDomainSettings,
WaitingForOctreeDataNegotation,
Running
};
/// Handles assignments of type OctreeServer - sending octrees to various clients.
class OctreeServer : public ThreadedAssignment, public HTTPRequestHandler {
Q_OBJECT
@ -36,6 +46,8 @@ public:
OctreeServer(ReceivedMessage& message);
~OctreeServer();
OctreeServerState _state { OctreeServerState::WaitingForDomainSettings };
/// allows setting of run arguments
void setArguments(int argc, char** argv);
@ -137,8 +149,7 @@ private slots:
void domainSettingsRequestComplete();
void handleOctreeQueryPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleOctreeDataNackPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleOctreeFileReplacement(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacementFromURL(QSharedPointer<ReceivedMessage> message);
void handleOctreeDataFileReply(QSharedPointer<ReceivedMessage> message);
void removeSendThread();
protected:
@ -159,12 +170,12 @@ protected:
QString getFileLoadTime();
QString getConfiguration();
QString getStatusLink();
void beginRunning(QByteArray replaceData);
UniqueSendThread createSendThread(const SharedNodePointer& node);
virtual UniqueSendThread newSendThread(const SharedNodePointer& node);
void replaceContentFromMessageData(QByteArray content);
int _argc;
const char** _argv;
char** _parsedArgV;

View file

@ -178,7 +178,7 @@ void EntityScriptServer::updateEntityPPS() {
int numRunningScripts = _entitiesScriptEngine->getNumRunningEntityScripts();
int pps;
if (std::numeric_limits<int>::max() / _entityPPSPerScript < numRunningScripts) {
qWarning() << QString("Integer multiplaction would overflow, clamping to maxint: %1 * %2").arg(numRunningScripts).arg(_entityPPSPerScript);
qWarning() << QString("Integer multiplication would overflow, clamping to maxint: %1 * %2").arg(numRunningScripts).arg(_entityPPSPerScript);
pps = std::numeric_limits<int>::max();
pps = std::min(_maxEntityPPS, pps);
} else {

View file

@ -1,6 +1,3 @@
if (NOT ANDROID)
endif()
#
# Copyright 2015 High Fidelity, Inc.
# Created by Bradley Austin Davis on 2015/10/10

View file

@ -22,7 +22,21 @@ setup_memory_debugger()
symlink_or_copy_directory_beside_target(${_SHOULD_SYMLINK_RESOURCES} "${CMAKE_CURRENT_SOURCE_DIR}/resources" "resources")
# link the shared hifi libraries
link_hifi_libraries(embedded-webserver networking shared avatars)
include_hifi_library_headers(gpu)
include_hifi_library_headers(graphics)
link_hifi_libraries(embedded-webserver networking shared avatars octree)
target_zlib()
add_dependency_external_projects(quazip)
find_package(QuaZip REQUIRED)
target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${QUAZIP_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES})
if (WIN32)
add_paths_to_fixup_libs(${QUAZIP_DLL_PATH})
endif ()
# find OpenSSL
find_package(OpenSSL REQUIRED)

View file

@ -1,12 +1,6 @@
{
"version": 2.1,
"version": 2.2,
"settings": [
{
"name": "label",
"label": "Label",
"settings": [
]
},
{
"name": "metaverse",
"label": "Metaverse / Networking",
@ -15,7 +9,8 @@
"name": "access_token",
"label": "Access Token",
"help": "This is your OAuth access token to connect this domain-server with your High Fidelity account. <br/>It can be generated by clicking the 'Connect Account' button above.<br/>You can also go to the <a href='https://metaverse.highfidelity.com/user/security' target='_blank'>My Security</a> page of your account and generate a token with the 'domains' scope and paste it here.",
"advanced": true
"advanced": true,
"backup": false
},
{
"name": "id",
@ -55,8 +50,8 @@
]
},
{
"label": "Places / Paths",
"html_id": "places_paths",
"label": "Paths",
"html_id": "paths",
"restart": false,
"settings": [
{
@ -64,6 +59,7 @@
"label": "Paths",
"help": "Clients can enter a path to reach an exact viewpoint in your domain.<br/>Add rows to the table below to map a path to a viewpoint.<br/>The index path ( / ) is where clients will enter if they do not enter an explicit path.",
"type": "table",
"content_setting": true,
"can_add_new_rows": true,
"key": {
"name": "path",
@ -164,7 +160,8 @@
{
"name": "http_username",
"label": "HTTP Username",
"help": "Username used for basic HTTP authentication."
"help": "Username used for basic HTTP authentication.",
"backup": false
},
{
"name": "http_password",
@ -172,7 +169,8 @@
"type": "password",
"help": "Password used for basic HTTP authentication. Leave this alone if you do not want to change it.",
"password_placeholder": "******",
"value-hidden": true
"value-hidden": true,
"backup": false
},
{
"name": "verify_http_password",
@ -308,7 +306,37 @@
}
],
"non-deletable-row-key": "permissions_id",
"non-deletable-row-values": [ "localhost", "anonymous", "logged-in" ]
"non-deletable-row-values": [ "localhost", "anonymous", "logged-in" ],
"default": [
{
"id_can_connect": true,
"id_can_rez_tmp_certified": true,
"permissions_id": "anonymous"
},
{
"id_can_connect": true,
"id_can_rez_tmp_certified": true,
"permissions_id": "friends"
},
{
"id_can_adjust_locks": true,
"id_can_connect": true,
"id_can_connect_past_max_capacity": true,
"id_can_kick": true,
"id_can_replace_content": true,
"id_can_rez": true,
"id_can_rez_certified": true,
"id_can_rez_tmp": true,
"id_can_rez_tmp_certified": true,
"id_can_write_to_asset_server": true,
"permissions_id": "localhost"
},
{
"id_can_connect": true,
"id_can_rez_tmp_certified": true,
"permissions_id": "logged-in"
}
]
},
{
"name": "group_permissions",
@ -935,6 +963,7 @@
"name": "persistent_scripts",
"type": "table",
"label": "Persistent Scripts",
"content_setting": true,
"help": "Add the URLs for scripts that you would like to ensure are always running in your domain.",
"can_add_new_rows": true,
"columns": [
@ -955,99 +984,6 @@
}
]
},
{
"name": "asset_server",
"label": "Asset Server (ATP)",
"assignment-types": [ 3 ],
"settings": [
{
"name": "enabled",
"type": "checkbox",
"label": "Enabled",
"help": "Assigns an asset-server in your domain to serve files to clients via the ATP protocol (over UDP)",
"default": true,
"advanced": true
},
{
"name": "assets_path",
"type": "string",
"label": "Assets Path",
"help": "The path to the directory assets are stored in.<br/>If this path is relative, it will be relative to the application data directory.<br/>If you change this path you will need to manually copy any existing assets from the previous directory.",
"default": "",
"advanced": true
},
{
"name": "assets_filesize_limit",
"type": "int",
"label": "File Size Limit",
"help": "The file size limit of an asset that can be imported into the asset server in MBytes. 0 (default) means no limit on file size.",
"default": 0,
"advanced": true
}
]
},
{
"name": "entity_script_server",
"label": "Entity Script Server (ESS)",
"assignment-types": [ 5 ],
"settings": [
{
"name": "entity_pps_per_script",
"label": "Entity PPS per script",
"help": "The number of packets per second (PPS) that can be sent to the entity server for each server entity script. This contributes to a total overall amount.<br/>Example: 1000 PPS with 5 entites gives a total PPS of 5000 that is shared among the entity scripts. A single could use 4000 PPS, leaving 1000 for the rest, for example.",
"default": 900,
"type": "int",
"advanced": true
},
{
"name": "max_total_entity_pps",
"label": "Maximum Total Entity PPS",
"help": "The maximum total packets per seconds (PPS) that can be sent to the entity server.<br/>Example: 5 scripts @ 1000 PPS per script = 5000 total PPS. A maximum total PPS of 4000 would cap this 5000 total PPS to 4000.",
"default": 9000,
"type": "int",
"advanced": true
}
]
},
{
"name": "avatars",
"label": "Avatars",
"assignment-types": [ 1, 2 ],
"settings": [
{
"name": "min_avatar_height",
"type": "double",
"label": "Minimum Avatar Height (meters)",
"help": "Limits the height of avatars in your domain. Must be at least 0.009.",
"placeholder": 0.4,
"default": 0.4
},
{
"name": "max_avatar_height",
"type": "double",
"label": "Maximum Avatar Height (meters)",
"help": "Limits the scale of avatars in your domain. Cannot be greater than 1755.",
"placeholder": 5.2,
"default": 5.2
},
{
"name": "avatar_whitelist",
"label": "Avatars Allowed from:",
"help": "Comma separated list of URLs (with optional paths) that avatar .fst files are allowed from. If someone attempts to use an avatar with a different domain, it will be rejected and the replacement avatar will be used. If left blank, any domain is allowed.",
"placeholder": "",
"default": "",
"advanced": true
},
{
"name": "replacement_avatar",
"label": "Replacement Avatar for disallowed avatars",
"help": "A URL for an avatar .fst to be used when someone tries to use an avatar that is not allowed. If left blank, the generic default avatar is used.",
"placeholder": "",
"default": "",
"advanced": true
}
]
},
{
"name": "audio_threading",
"label": "Audio Threading",
@ -1080,6 +1016,7 @@
"name": "attenuation_per_doubling_in_distance",
"label": "Default Domain Attenuation",
"help": "Factor between 0 and 1.0 (0: No attenuation, 1.0: extreme attenuation)",
"content_setting": true,
"placeholder": "0.5",
"default": "0.5",
"advanced": false
@ -1105,6 +1042,7 @@
"label": "Zones",
"help": "In this table you can define a set of zones in which you can specify various audio properties.",
"numbered": false,
"content_setting": true,
"can_add_new_rows": true,
"key": {
"name": "name",
@ -1155,6 +1093,7 @@
"type": "table",
"label": "Attenuation Coefficients",
"help": "In this table you can set custom attenuation coefficients between audio zones",
"content_setting": true,
"numbered": true,
"can_order": true,
"can_add_new_rows": true,
@ -1185,6 +1124,7 @@
"label": "Reverb Settings",
"help": "In this table you can set reverb levels for audio zones. For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet/dry mix of 25%. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet/dry mix of 50%.",
"numbered": true,
"content_setting": true,
"can_add_new_rows": true,
"columns": [
{
@ -1266,9 +1206,82 @@
}
]
},
{
"name": "avatars",
"label": "Avatars",
"assignment-types": [ 1, 2 ],
"settings": [
{
"name": "min_avatar_height",
"type": "double",
"label": "Minimum Avatar Height (meters)",
"help": "Limits the height of avatars in your domain. Must be at least 0.009.",
"placeholder": 0.4,
"default": 0.4
},
{
"name": "max_avatar_height",
"type": "double",
"label": "Maximum Avatar Height (meters)",
"help": "Limits the scale of avatars in your domain. Cannot be greater than 1755.",
"placeholder": 5.2,
"default": 5.2
},
{
"name": "avatar_whitelist",
"label": "Avatars Allowed from:",
"help": "Comma separated list of URLs (with optional paths) that avatar .fst files are allowed from. If someone attempts to use an avatar with a different domain, it will be rejected and the replacement avatar will be used. If left blank, any domain is allowed.",
"placeholder": "",
"default": "",
"advanced": true
},
{
"name": "replacement_avatar",
"label": "Replacement Avatar for disallowed avatars",
"help": "A URL for an avatar .fst to be used when someone tries to use an avatar that is not allowed. If left blank, the generic default avatar is used.",
"placeholder": "",
"default": "",
"advanced": true
}
]
},
{
"name": "avatar_mixer",
"label": "Avatar Mixer",
"assignment-types": [
1
],
"settings": [
{
"name": "max_node_send_bandwidth",
"type": "double",
"label": "Per-Node Bandwidth",
"help": "Desired maximum send bandwidth (in Megabits per second) to each node",
"placeholder": 5.0,
"default": 5.0,
"advanced": true
},
{
"name": "auto_threads",
"label": "Automatically determine thread count",
"type": "checkbox",
"help": "Allow system to determine number of threads (recommended)",
"default": false,
"advanced": true
},
{
"name": "num_threads",
"label": "Number of Threads",
"help": "Threads to spin up for avatar mixing (if not automatically set)",
"placeholder": "1",
"default": "1",
"advanced": true
}
]
},
{
"name": "entity_server_settings",
"label": "Entity Server Settings",
"label": "Entities",
"assignment-types": [
6
],
@ -1309,6 +1322,7 @@
"name": "entityEditFilter",
"label": "Filter Entity Edits",
"help": "Check all entity edits against this filter function.",
"content_setting": true,
"placeholder": "url whose content is like: function filter(properties) { return properties; }",
"default": "",
"advanced": true
@ -1337,73 +1351,6 @@
"default": "30000",
"advanced": true
},
{
"name": "backups",
"type": "table",
"label": "Backup Rules",
"help": "In this table you can define a set of rules for how frequently to backup copies of your entites content file.",
"numbered": false,
"can_add_new_rows": true,
"default": [
{
"Name": "Half Hourly Rolling",
"backupInterval": 1800,
"format": ".backup.halfhourly.%N",
"maxBackupVersions": 5
},
{
"Name": "Daily Rolling",
"backupInterval": 86400,
"format": ".backup.daily.%N",
"maxBackupVersions": 7
},
{
"Name": "Weekly Rolling",
"backupInterval": 604800,
"format": ".backup.weekly.%N",
"maxBackupVersions": 4
},
{
"Name": "Thirty Day Rolling",
"backupInterval": 2592000,
"format": ".backup.thirtyday.%N",
"maxBackupVersions": 12
}
],
"columns": [
{
"name": "Name",
"label": "Name",
"can_set": true,
"placeholder": "Example",
"default": "Example"
},
{
"name": "format",
"label": "Rule Format",
"can_set": true,
"help": "Format used to create the extension for the backup of your persisted entities. Use a format with %N to get rolling. Or use date formatting like %Y-%m-%d.%H:%M:%S.%z",
"placeholder": ".backup.example.%N",
"default": ".backup.example.%N"
},
{
"name": "backupInterval",
"label": "Backup Interval in Seconds",
"help": "Interval between backup checks in seconds.",
"placeholder": 1800,
"default": 1800,
"can_set": true
},
{
"name": "maxBackupVersions",
"label": "Max Rolled Backup Versions",
"help": "If your backup extension format uses 'rolling', how many versions do you want us to keep?",
"placeholder": 5,
"default": 5,
"can_set": true
}
]
},
{
"name": "NoPersist",
"type": "checkbox",
@ -1503,35 +1450,55 @@
]
},
{
"name": "avatar_mixer",
"label": "Avatar Mixer",
"assignment-types": [
1
],
"name": "asset_server",
"label": "Asset Server (ATP)",
"assignment-types": [ 3 ],
"settings": [
{
"name": "max_node_send_bandwidth",
"type": "double",
"label": "Per-Node Bandwidth",
"help": "Desired maximum send bandwidth (in Megabits per second) to each node",
"placeholder": 5.0,
"default": 5.0,
"advanced": true
},
{
"name": "auto_threads",
"label": "Automatically determine thread count",
"name": "enabled",
"type": "checkbox",
"help": "Allow system to determine number of threads (recommended)",
"default": false,
"label": "Enabled",
"help": "Assigns an asset-server in your domain to serve files to clients via the ATP protocol (over UDP)",
"default": true,
"advanced": true
},
{
"name": "num_threads",
"label": "Number of Threads",
"help": "Threads to spin up for avatar mixing (if not automatically set)",
"placeholder": "1",
"default": "1",
"name": "assets_path",
"type": "string",
"label": "Assets Path",
"help": "The path to the directory assets are stored in.<br/>If this path is relative, it will be relative to the application data directory.<br/>If you change this path you will need to manually copy any existing assets from the previous directory.",
"default": "",
"advanced": true
},
{
"name": "assets_filesize_limit",
"type": "int",
"label": "File Size Limit",
"help": "The file size limit of an asset that can be imported into the asset server in MBytes. 0 (default) means no limit on file size.",
"default": 0,
"advanced": true
}
]
},
{
"name": "entity_script_server",
"label": "Entity Script Server (ESS)",
"assignment-types": [ 5 ],
"settings": [
{
"name": "entity_pps_per_script",
"label": "Entity PPS per script",
"help": "The number of packets per second (PPS) that can be sent to the entity server for each server entity script. This contributes to a total overall amount.<br/>Example: 1000 PPS with 5 entites gives a total PPS of 5000 that is shared among the entity scripts. A single could use 4000 PPS, leaving 1000 for the rest, for example.",
"default": 900,
"type": "int",
"advanced": true
},
{
"name": "max_total_entity_pps",
"label": "Maximum Total Entity PPS",
"help": "The maximum total packets per seconds (PPS) that can be sent to the entity server.<br/>Example: 5 scripts @ 1000 PPS per script = 5000 total PPS. A maximum total PPS of 4000 would cap this 5000 total PPS to 4000.",
"default": 9000,
"type": "int",
"advanced": true
}
]
@ -1645,6 +1612,67 @@
}
]
},
{
"name": "automatic_content_archives",
"label": "Automatic Content Archives",
"settings": [
{
"name": "backup_rules",
"type": "table",
"label": "Rolling Backup Rules",
"help": "Define how frequently to create automatic content archives",
"numbered": false,
"can_add_new_rows": true,
"default": [
{
"Name": "Half Hourly Rolling",
"backupInterval": 1800,
"maxBackupVersions": 5
},
{
"Name": "Daily Rolling",
"backupInterval": 86400,
"maxBackupVersions": 7
},
{
"Name": "Weekly Rolling",
"backupInterval": 604800,
"maxBackupVersions": 4
},
{
"Name": "Thirty Day Rolling",
"backupInterval": 2592000,
"maxBackupVersions": 12
}
],
"columns": [
{
"name": "Name",
"label": "Name",
"can_set": true,
"placeholder": "Example",
"default": "Example"
},
{
"name": "backupInterval",
"label": "Backup Interval in Seconds",
"help": "Interval between backup checks in seconds.",
"placeholder": 1800,
"default": 1800,
"can_set": true
},
{
"name": "maxBackupVersions",
"label": "Max Rolled Backup Versions",
"help": "If your backup extension format uses 'rolling', how many versions do you want us to keep?",
"placeholder": 5,
"default": 5,
"can_set": true
}
]
}
]
},
{
"name": "wizard",
"label": "Setup Wizard",

View file

@ -0,0 +1,6 @@
<script src='/js/underscore-min.js'></script>
<script src='/js/underscore-keypath.min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='/js/form2js.min.js'></script>
<script src='/js/bootstrap-switch.min.js'></script>
<script src='/js/base-settings.js'></script>

View file

@ -0,0 +1,59 @@
<div class="col-md-10 col-md-offset-1 col-xs-12">
<div class="row">
<div class="col-xs-12">
<div id="cloud-domains-alert" class="alert alert-info alert-dismissible" role="alert" style="display: none;">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span class="alert-link">
<a href="https://highfidelity.com/user/cloud_domains" target="_blank" class="blue-link">Visit Cloud Hosted Domains</a> to manage all your cloud domains
</span>
</div>
<form id="settings-form" role="form">
<script id="panels-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% if (!group.hidden) { %>
<% var settings = _.partition(group.settings, function(value, index) { %>
<% return !value.deprecated %>
<% })[0] %>
<% var split_settings = _.partition(group.settings, function(value, index) { %>
<% return !value.advanced %>
<% }) %>
<% isGrouped = !!group.name %>
<% panelID = isGrouped ? group.name : group.html_id %>
<div id="<%- panelID %>_group" class="anchor"></div>
<div class="panel panel-default<%- (isGrouped) ? ' grouped' : '' %>"
id="<%- panelID %>">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
<span class="badge"></span>
</div>
<div class="panel-body">
<% _.each(split_settings[0], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, false) %>
<% }); %>
<% if (split_settings[1].length > 0) { %>
<button type="button" class="btn btn-default" data-toggle="collapse" data-target="#<%- panelID %>-advanced">Advanced Settings <span class="caret"></span></button>
<div id="<%- panelID %>-advanced" class="collapse advanced-settings-section">
<% _.each(split_settings[1], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, true) %>
<% }); %>
</div>
<% } %>
</div>
</div>
<% } %>
<% }); %>
</script>
<div id="panels"></div>
</form>
</div>
</div>
</div>

View file

@ -1,45 +1,21 @@
<!--#include virtual="header.html"-->
<div class="col-md-10 col-md-offset-1">
<div class="row">
<div class="col-xs-12">
<div class="alert" style="display:none;"></div>
</div>
</div>
<script type="text/javascript">
var Settings = {
content_settings: true,
endpoint: "/content-settings.json",
path: "/content/"
};
</script>
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Upload Entities File</h3>
</div>
<form id="upload-form" action="upload" enctype="multipart/form-data" method="post">
<div class="panel-body">
<p>
Upload an entities file (e.g.: models.json.gz) to replace the content of this domain.<br>
Note: <strong>Your domain's content will be replaced by the content you upload</strong>, but the backup files of your domain's content will not immediately be changed.
</p>
<p>If your domain has any content that you would like to re-use at a later date, save a manual backup of your models.json.gz file, which is usually stored at the following paths:</p>
<label class="control-label">Windows</label>
<pre>C:/Users/[username]/AppData/Roaming/High Fidelity/assignment-client/entities/models.json.gz</pre>
<label class="control-label">OSX</label>
<pre>/Users/[username]/Library/Application Support/High Fidelity/assignment-client/entities/models.json.gz</pre>
<label class="control-label">Linux</label>
<pre>/home/[username]/.local/share/High Fidelity/assignment-client/entities/models.json.gz</pre>
<br>
<input type="file" name="entities-file" class="form-control-file" accept=".json, .gz">
<br>
</div>
<div class="panel-footer">
<input type="submit" class="btn btn-info" value="Upload">
</div>
</form>
</div>
</div>
</div>
</div>
<!--#include virtual="base-settings.html"-->
<!--#include virtual="footer.html"-->
<script src='js/content.js'></script>
<script src='/js/sweetalert.min.js'></script>
<!--#include virtual="base-settings-scripts.html"-->
<script src="js/moment-locale.min.js"></script>
<script src="js/bootstrap-sortable.min.js"></script>
<script src="js/content.js"></script>
<!--#include virtual="page-end.html"-->

File diff suppressed because one or more lines are too long

View file

@ -1,45 +1,437 @@
$(document).ready(function(){
function showSpinnerAlert(title) {
swal({
title: title,
text: '<div class="spinner" style="color:black;"><div class="bounce1"></div><div class="bounce2"></div><div class="bounce3"></div></div>',
html: true,
showConfirmButton: false,
allowEscapeKey: false
var RESTORE_SETTINGS_UPLOAD_ID = 'restore-settings-button';
var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file';
var UPLOAD_CONTENT_ALLOWED_DIV_ID = 'upload-content-allowed';
var UPLOAD_CONTENT_RECOVERING_DIV_ID = 'upload-content-recovering';
var isRestoring = false;
function progressBarHTML(extraClass, label) {
var html = "<div class='progress'>";
html += "<div class='" + extraClass + " progress-bar progress-bar-success progress-bar-striped active' role='progressbar' aria-valuemin='0' aria-valuemax='100'>";
html += label + "<span class='sr-only'></span></div></div>";
return html;
}
function setupBackupUpload() {
// construct the HTML needed for the settings backup panel
var html = "<div class='form-group'><div id='" + UPLOAD_CONTENT_ALLOWED_DIV_ID + "'>";
html += "<span class='help-block'>Upload a content archive (.zip) or entity file (.json, .json.gz) to replace the content of this domain.";
html += "<br/>Note: Your domain content will be replaced by the content you upload, but the existing backup files of your domain's content will not immediately be changed.</span>";
html += "<input id='restore-settings-file' name='restore-settings' type='file'>";
html += "<button type='button' id='" + RESTORE_SETTINGS_UPLOAD_ID + "' disabled='true' class='btn btn-primary'>Upload Content</button>";
html += "</div><div id='" + UPLOAD_CONTENT_RECOVERING_DIV_ID + "'>";
html += "<span class='help-block'>Restore in progress</span>";
html += progressBarHTML('recovery', 'Restoring');
html += "</div></div>";
$('#' + Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID + ' .panel-body').html(html);
}
// handle content archive or entity file upload
// when the selected file is changed, enable the button if there's a selected file
$('body').on('change', '#' + RESTORE_SETTINGS_FILE_ID, function() {
$('#' + RESTORE_SETTINGS_UPLOAD_ID).attr('disabled', $(this).val().length == 0);
});
// when the upload button is clicked, send the file to the DS
// and reload the page if restore was successful or
// show an error if not
$('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){
e.preventDefault();
swalAreYouSure(
"Your domain content will be replaced by the uploaded content archive or entity file",
"Restore content",
function() {
var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files');
var fileFormData = new FormData();
fileFormData.append('restore-file', files[0]);
showSpinnerAlert("Uploading content to restore");
$.ajax({
url: '/content/upload',
type: 'POST',
cache: false,
processData: false,
contentType: false,
data: fileFormData
}).done(function(data, textStatus, jqXHR) {
isRestoring = true;
// immediately reload backup information since one should be restoring now
reloadBackupInformation();
swal.close();
}).fail(function(jqXHR, textStatus, errorThrown) {
showErrorMessage(
"Error",
"There was a problem restoring domain content.\n"
+ "Please ensure that the content archive or entity file is valid and try again."
);
});
}
);
});
var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button';
var CONTENT_ARCHIVES_NORMAL_ID = 'content-archives-success';
var CONTENT_ARCHIVES_ERROR_ID = 'content-archives-error';
var AUTOMATIC_ARCHIVES_TABLE_ID = 'automatic-archives-table';
var AUTOMATIC_ARCHIVES_TBODY_ID = 'automatic-archives-tbody';
var MANUAL_ARCHIVES_TABLE_ID = 'manual-archives-table';
var MANUAL_ARCHIVES_TBODY_ID = 'manual-archives-tbody';
var AUTO_ARCHIVES_SETTINGS_LINK_ID = 'auto-archives-settings-link';
var ACTION_MENU_CLASS = 'action-menu';
var automaticBackups = [];
var manualBackups = [];
function setupContentArchives() {
// construct the HTML needed for the content archives panel
var html = "<div id='" + CONTENT_ARCHIVES_NORMAL_ID + "'><div class='form-group'>";
html += "<label class='control-label'>Automatic Content Archives</label>";
html += "<span class='help-block'>Your domain server makes regular archives of the content in your domain. In the list below, you can see and download all of your backups of domain content and content settings."
html += "<a href='/settings/#automatic_content_archives' id='" + AUTO_ARCHIVES_SETTINGS_LINK_ID + "'>Click here to manage automatic content archive intervals.</a></span>";
html += "</div>";
html += "<table class='table sortable' id='" + AUTOMATIC_ARCHIVES_TABLE_ID + "'>";
var backups_table_head = "<thead><tr class='gray-tr'><th>Archive Name</th><th data-defaultsort='desc'>Archive Date</th>"
+ "<th data-defaultsort='disabled'></th><th class='" + ACTION_MENU_CLASS + "' data-defaultsort='disabled'>Actions</th>"
+ "</tr></thead>";
html += backups_table_head;
html += "<tbody id='" + AUTOMATIC_ARCHIVES_TBODY_ID + "'></tbody></table>";
html += "<div class='form-group'>";
html += "<label class='control-label'>Manual Content Archives</label>";
html += "<span class='help-block'>You can generate and download an archive of your domain content right now. You can also download, delete and restore any archive listed.</span>";
html += "<button type='button' id='" + GENERATE_ARCHIVE_BUTTON_ID + "' class='btn btn-primary'>Generate New Archive</button>";
html += "</div>";
html += "<table class='table sortable' id='" + MANUAL_ARCHIVES_TABLE_ID + "'>";
html += backups_table_head;
html += "<tbody id='" + MANUAL_ARCHIVES_TBODY_ID + "'></tbody></table></div>";
html += "<div class='form-group' id='" + CONTENT_ARCHIVES_ERROR_ID + "' style='display:none;'>"
+ "<span class='help-block'>There was a problem loading your list of automatic and manual content archives. "
+ "Please reload the page to try again.</span></div>";
// put the base HTML in the content archives panel
$('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html(html);
}
var BACKUP_RESTORE_LINK_CLASS = 'restore-backup';
var BACKUP_DOWNLOAD_LINK_CLASS = 'download-backup';
var BACKUP_DELETE_LINK_CLASS = 'delete-backup';
var ACTIVE_BACKUP_ROW_CLASS = 'active-backup';
var CORRUPTED_ROW_CLASS = 'danger';
function reloadBackupInformation() {
// make a GET request to get backup information to populate the table
$.ajax({
url: '/api/backups',
cache: false
}).done(function(data) {
// split the returned data into manual and automatic manual backups
var splitBackups = _.partition(data.backups, function(value, index) {
return value.isManualBackup;
});
if (isRestoring && !data.status.isRecovering) {
// we were recovering and we finished - the DS is going to restart so show the restart modal
showRestartModal();
return;
}
isRestoring = data.status.isRecovering;
manualBackups = splitBackups[0];
automaticBackups = splitBackups[1];
// populate the backups tables with the backups
function createBackupTableRow(backup) {
return "<tr data-backup-id='" + backup.id + "' data-backup-name='" + backup.name + "'>"
+ "<td data-value='" + backup.name.toLowerCase() + "'>" + backup.name + "</td><td data-value='" + backup.createdAtMillis + "'>"
+ moment(backup.createdAtMillis).format('lll')
+ "</td><td class='backup-status'></td><td class='" + ACTION_MENU_CLASS + "'>"
+ "<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_DELETE_LINK_CLASS + "' href='#' target='_blank'>Delete</a></li></ul></div></td>";
}
function updateProgressBars($progressBar, value) {
$progressBar.attr('aria-valuenow', value).attr('style', 'width: ' + value + '%');
$progressBar.find('.sr-only').html(value + "% Complete");
}
// before we add any new rows and update existing ones
// remove our flag for active rows
$('.' + ACTIVE_BACKUP_ROW_CLASS).removeClass(ACTIVE_BACKUP_ROW_CLASS);
function updateOrAddTableRow(backup, tableBodyID) {
// check for a backup with this ID
var $backupRow = $("tr[data-backup-id='" + backup.id + "']");
if ($backupRow.length == 0) {
// create a new row and then add it to the table
$backupRow = $(createBackupTableRow(backup));
$('#' + tableBodyID).append($backupRow);
}
// update the row status column depending on if it is available or recovering
if (!backup.isAvailable) {
// add a progress bar to the status row for availability
$backupRow.find('td.backup-status').html(progressBarHTML('availability', 'Archiving'));
// set the value of the progress bar based on availability progress
updateProgressBars($backupRow.find('.progress-bar'), backup.availabilityProgress * 100);
} else if (backup.id == data.status.recoveringBackupId) {
// add a progress bar to the status row for recovery
$backupRow.find('td.backup-status').html(progressBarHTML('recovery', 'Restoring'));
} else if (backup.isCorrupted) {
// add text for corrupted status to row
$backupRow.find('td.backup-status').html('<span>Corrupted</span>');
} else {
// no special status for this row, use an empty status column
$backupRow.find('td.backup-status').html('');
}
// color the row red if it is corrupted
$backupRow.toggleClass(CORRUPTED_ROW_CLASS, backup.isCorrupted);
// disable restore if the backup is corrupted
$backupRow.find('a.' + BACKUP_RESTORE_LINK_CLASS).parent().toggleClass('disabled', backup.isCorrupted);
// toggle the dropdown menu depending on if the row is available
$backupRow.find('td.' + ACTION_MENU_CLASS + ' .dropdown').toggle(backup.isAvailable);
$backupRow.addClass(ACTIVE_BACKUP_ROW_CLASS);
}
if (automaticBackups.length > 0) {
for (var backupIndex in automaticBackups) {
updateOrAddTableRow(automaticBackups[backupIndex], AUTOMATIC_ARCHIVES_TBODY_ID);
}
}
if (manualBackups.length > 0) {
for (var backupIndex in manualBackups) {
updateOrAddTableRow(manualBackups[backupIndex], MANUAL_ARCHIVES_TBODY_ID);
}
}
// at this point, any rows that no longer have the ACTIVE_BACKUP_ROW_CLASS
// are deleted backups, so we remove them from the table
$('#' + CONTENT_ARCHIVES_NORMAL_ID + ' tbody tr:not(.' + ACTIVE_BACKUP_ROW_CLASS + ')').remove();
// check if the restore action on all rows should be enabled or disabled
$('tr:not(.' + CORRUPTED_ROW_CLASS + ') .' + BACKUP_RESTORE_LINK_CLASS).parent().toggleClass('disabled', data.status.isRecovering);
// hide or show the manual content upload file and button depending on our recovering status
$('#' + UPLOAD_CONTENT_ALLOWED_DIV_ID).toggle(!data.status.isRecovering);
$('#' + UPLOAD_CONTENT_RECOVERING_DIV_ID).toggle(data.status.isRecovering);
// update the progress bars for current restore status
if (data.status.isRecovering) {
updateProgressBars($('.recovery.progress-bar'), data.status.recoveryProgress * 100);
}
// tell bootstrap sortable to update for the new rows
$.bootstrapSortable({ applyLast: true });
$('#' + CONTENT_ARCHIVES_NORMAL_ID).toggle(true);
$('#' + CONTENT_ARCHIVES_ERROR_ID).toggle(false);
}).fail(function(){
// we've hit the very rare case where we couldn't load the list of backups from the domain server
// set our backups to empty
automaticBackups = [];
manualBackups = [];
// replace the content archives panel with a simple error message
// stating that the user should reload the page
$('#' + CONTENT_ARCHIVES_NORMAL_ID).toggle(false);
$('#' + CONTENT_ARCHIVES_ERROR_ID).toggle(true);
}).always(function(){
// toggle showing or hiding the tables depending on if they have entries
$('#' + AUTOMATIC_ARCHIVES_TABLE_ID).toggle(automaticBackups.length > 0);
$('#' + MANUAL_ARCHIVES_TABLE_ID).toggle(manualBackups.length > 0);
});
}
var frm = $('#upload-form');
frm.submit(function (ev) {
$.ajax({
type: frm.attr('method'),
url: frm.attr('action'),
data: new FormData($(this)[0]),
cache: false,
contentType: false,
processData: false,
success: function (data) {
swal({
title: 'Uploaded',
type: 'success',
text: 'Your Entity Server is restarting to replace its local content with the uploaded file.',
confirmButtonText: 'OK'
})
},
error: function (data) {
swal({
title: '',
type: 'error',
text: 'Your entities file could not be transferred to the Entity Server.</br>Verify that the file is a <i>.json</i> or <i>.json.gz</i> entities file and try again.',
html: true,
confirmButtonText: 'OK',
// handle click in table to restore a given content backup
$('body').on('click', '.' + BACKUP_RESTORE_LINK_CLASS, function(e) {
// stop the default behaviour
e.preventDefault();
// if this is a disabled link, don't proceed with the restore
if ($(this).parent().hasClass('disabled')) {
return false;
}
// grab the name of this backup so we can show it in alerts
var backupName = $(this).closest('tr').attr('data-backup-name');
// grab the ID of this backup in case we need to send a POST
var backupID = $(this).closest('tr').attr('data-backup-id');
// make sure the user knows what is about to happen
swalAreYouSure(
"Your domain content will be replaced by the content archive " + backupName,
"Restore content",
function() {
// show a spinner while we send off our request
showSpinnerAlert("Starting restore of " + backupName);
// setup an AJAX POST to request content restore
$.post('/api/backups/recover/' + backupID).done(function(data, textStatus, jqXHR) {
isRestoring = true;
// immediately reload our backup information since one should be restoring now
reloadBackupInformation();
swal.close();
}).fail(function(jqXHR, textStatus, errorThrown) {
showErrorMessage(
"Error",
"There was a problem restoring domain content.\n"
+ "If the problem persists, the content archive may be corrupted."
);
});
}
});
ev.preventDefault();
showSpinnerAlert("Uploading Entities File");
)
});
// handle click in table to delete a given content backup
$('body').on('click', '.' + BACKUP_DELETE_LINK_CLASS, function(e){
// stop the default behaviour
e.preventDefault();
// grab the name of this backup so we can show it in alerts
var backupName = $(this).closest('tr').attr('data-backup-name');
// grab the ID of this backup in case we need to send the DELETE request
var backupID = $(this).closest('tr').attr('data-backup-id');
// make sure the user knows what is about to happen
swalAreYouSure(
"The content archive " + backupName + " will be deleted and will no longer be available for restore or download from this page.",
"Delete content archive",
function() {
// show a spinner while we send off our request
showSpinnerAlert("Deleting content archive " + backupName);
// setup an AJAX DELETE to request content archive delete
$.ajax({
url: '/api/backups/' + backupID,
type: 'DELETE'
}).done(function(data, textStatus, jqXHR) {
swal.close();
}).fail(function(jqXHR, textStatus, errorThrown) {
showErrorMessage(
"Error",
"There was an unexpected error deleting the content archive"
);
}).always(function(){
// reload the list of content archives in case we deleted a backup
// or it's no longer an available backup for some other reason
reloadBackupInformation();
});
}
)
});
// handle click on automatic content archive settings link
$('body').on('click', '#' + AUTO_ARCHIVES_SETTINGS_LINK_ID, function(e) {
if (Settings.pendingChanges > 0) {
// don't follow the link right away, make sure the user knows they are about to leave
// the page and lose changes
e.preventDefault();
var settingsLink = $(this).attr('href');
swalAreYouSure(
"You have pending changes to content settings that have not been saved. They will be lost if you leave the page to manage automatic content archive intervals.",
"Proceed without Saving",
function() {
// user wants to drop their changes, switch pages
window.location = settingsLink;
}
);
}
});
// handle click on manual archive creation button
$('body').on('click', '#' + GENERATE_ARCHIVE_BUTTON_ID, function(e) {
e.preventDefault();
// show a sweet alert to ask the user to provide a name for their content archive
swal({
title: "Generate a content archive",
type: "input",
text: "This will capture the state of all the content in your domain right now, which you can save as a backup and restore from later.",
confirmButtonText: "Generate Archive",
showCancelButton: true,
closeOnConfirm: false,
inputPlaceholder: 'Archive Name'
}, function(inputValue){
if (inputValue === false) {
return false;
}
if (inputValue === "") {
swal.showInputError("Please give the content archive a name.")
return false;
}
var MANUAL_ARCHIVE_NAME_REGEX = /^[a-zA-Z0-9\-_ ]+$/;
if (!MANUAL_ARCHIVE_NAME_REGEX.test(inputValue)) {
swal.showInputError("Valid characters include A-z, 0-9, ' ', '_', and '-'.");
return false;
}
// post the provided archive name to ask the server to kick off a manual backup
$.ajax({
type: 'POST',
url: '/api/backups',
data: {
'name': inputValue
}
}).done(function(data) {
// since we successfully setup a new content archive, reload the table of archives
// which should show that this archive is pending creation
swal.close();
reloadBackupInformation();
}).fail(function(jqXHR, textStatus, errorThrown) {
showErrorMessage(
"Error",
"There was an unexpected error creating the manual content archive"
)
});
});
});
Settings.extraGroupsAtIndex = Settings.extraContentGroupsAtIndex;
Settings.afterReloadActions = function() {
setupBackupUpload();
setupContentArchives();
// load the latest backups immediately
reloadBackupInformation();
// setup a timer to reload them every 5 seconds
setInterval(reloadBackupInformation, 5000);
};
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,110 @@
/**
* adding sorting ability to HTML tables with Bootstrap styling
* @summary HTML tables sorting ability
* @version 2.0.0
* @requires tinysort, moment.js, jQuery
* @license MIT
* @author Matus Brlit (drvic10k)
* @copyright Matus Brlit (drvic10k), bootstrap-sortable contributors
*/
table.sortable span.sign {
display: block;
position: absolute;
top: 50%;
right: 5px;
font-size: 12px;
margin-top: -10px;
color: #bfbfc1;
}
table.sortable th:after {
display: block;
position: absolute;
top: 50%;
right: 5px;
font-size: 12px;
margin-top: -10px;
color: #bfbfc1;
}
table.sortable th.arrow:after {
content: '';
}
table.sortable span.arrow, span.reversed, th.arrow.down:after, th.reversedarrow.down:after, th.arrow.up:after, th.reversedarrow.up:after {
border-style: solid;
border-width: 5px;
font-size: 0;
border-color: #ccc transparent transparent transparent;
line-height: 0;
height: 0;
width: 0;
margin-top: -2px;
}
table.sortable span.arrow.up, th.arrow.up:after {
border-color: transparent transparent #ccc transparent;
margin-top: -7px;
}
table.sortable span.reversed, th.reversedarrow.down:after {
border-color: transparent transparent #ccc transparent;
margin-top: -7px;
}
table.sortable span.reversed.up, th.reversedarrow.up:after {
border-color: #ccc transparent transparent transparent;
margin-top: -2px;
}
table.sortable span.az:before, th.az.down:after {
content: "a .. z";
}
table.sortable span.az.up:before, th.az.up:after {
content: "z .. a";
}
table.sortable th.az.nosort:after, th.AZ.nosort:after, th._19.nosort:after, th.month.nosort:after {
content: "..";
}
table.sortable span.AZ:before, th.AZ.down:after {
content: "A .. Z";
}
table.sortable span.AZ.up:before, th.AZ.up:after {
content: "Z .. A";
}
table.sortable span._19:before, th._19.down:after {
content: "1 .. 9";
}
table.sortable span._19.up:before, th._19.up:after {
content: "9 .. 1";
}
table.sortable span.month:before, th.month.down:after {
content: "jan .. dec";
}
table.sortable span.month.up:before, th.month.up:after {
content: "dec .. jan";
}
table.sortable>thead th:not([data-defaultsort=disabled]) {
cursor: pointer;
position: relative;
top: 0;
left: 0;
}
table.sortable>thead th:hover:not([data-defaultsort=disabled]) {
background: #efefef;
}
table.sortable>thead th div.mozilla {
position: relative;
}

View file

@ -18,6 +18,14 @@ body {
margin-top: 70px;
}
/* unfortunate hack so that anchors go to the right place with fixed navbar */
:target:before {
content: " ";
display: block;
height: 70px;
margin-top: -70px;
}
[hidden] {
display: none !important;
}
@ -118,11 +126,6 @@ span.port {
margin-top: 10px;
}
#small-save-button {
width: 100%;
margin-bottom: 15px;
}
td.buttons {
width: 30px;
}
@ -345,3 +348,138 @@ table .headers + .headers td {
text-align: center;
margin-top: 20px;
}
@media (min-width: 768px) {
ul.nav li.dropdown-on-hover:hover ul.dropdown-menu {
display: block;
}
}
ul.dropdown-menu {
padding: 0px 0px;
}
ul.dropdown-menu li a {
padding-top: 7px;
padding-bottom: 7px;
}
ul.dropdown-menu li a:hover {
color: white;
background-color: #337ab7;
}
table ul.dropdown-menu li:first-child a:hover {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
ul.dropdown-menu li:last-child a:hover {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
ul.dropdown-menu .divider {
margin: 0px 0;
}
#visit-domain-link {
background-color: transparent;
}
.navbar-btn {
margin-left: 10px;
}
#save-settings-xs-button {
float: right;
margin-right: 10px;
}
#button-bars {
display: inline-block;
float: left;
}
#hamburger-badge {
position: relative;
top: -2px;
float: left;
margin-right: 10px;
margin-left: 0px;
}
#restart-server {
margin-left: 0px;
}
#restart-server:hover {
text-decoration: none;
}
.badge {
margin-left: 5px;
background-color: #00B4EF !important;
}
.panel-title {
display: inline-block;
}
#visit-hmd-icon {
width: 25px;
position: relative;
top: -1px;
}
.advanced-settings-section {
margin-top: 20px;
}
#restore-settings-button {
margin-top: 10px;
}
/* fix for https://bugs.webkit.org/show_bug.cgi?id=39620 */
.save-button-text {
pointer-events: none;
}
#content_archives .panel-body {
padding: 0;
}
#content_archives .panel-body .form-group {
padding: 15px;
}
#content_archives .panel-body th, #content_archives .panel-body td {
padding: 8px 15px;
}
#content_archives table {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
tr.gray-tr {
background-color: #f5f5f5;
}
table .action-menu {
text-align: right;
width: 90px;
}
.dropdown-toggle span.glyphicon-option-vertical {
font-size: 110%;
cursor: pointer;
border-radius: 50%;
background-color: #F5F5F5;
padding: 4px 4px 4px 6px;
}
.dropdown.open span.glyphicon-option-vertical {
background-color: #337AB7;
color: white;
}

View file

@ -1,4 +1,5 @@
</div>
<script src='/js/jquery-2.1.4.min.js'></script>
<script src='/js/bootstrap.min.js'></script>
<script src='/js/shared.js'></script>
<script src='/js/domain-server.js'></script>

View file

@ -9,6 +9,7 @@
<link href="/css/style.css" rel="stylesheet" media="screen">
<link href="/css/sweetalert.css" rel="stylesheet" media="screen">
<link href="/css/bootstrap-switch.min.css" rel="stylesheet" media="screen">
<link href="/css/bootstrap-sortable.css" rel="stylesheet" media="screen">
<script src='/js/sweetalert.min.js'></script>
</head>
@ -17,30 +18,47 @@
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#collapsed-navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span id="hamburger-badge" class="badge"></span>
<div id="button-bars">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</div>
</button>
<button id="save-settings-xs-button" class="save-button btn btn-success navbar-btn hidden-sm hidden-md hidden-lg" disabled="true"><span class="save-button-text">Save</span></button>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<div class="collapse navbar-collapse" id="collapsed-navbar">
<ul class="nav navbar-nav">
<li><a href="/">Nodes</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Assignments <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="/assignment">New Assignment</a></li>
</ul>
<li><a href="/assignment">Assignment</a></li>
<li class="dropdown dropdown-on-hover">
<a href="/content/" class="hidden-xs">Content <span class="content-settings-badge badge"></span> <span class="caret"></span></a>
<a href="#" class="dropdown-toggle hidden-sm hidden-md hidden-lg" data-toggle="dropdown">Content <span class="content-badge badge"></span> <span class="caret"></span></a>
<ul id="content-settings-nav-dropdown" class="dropdown-menu" role="menu">
</ul>
</li>
<li class="dropdown dropdown-on-hover">
<a href="/settings/" class="hidden-xs">Settings <span class="domain-settings-badge badge"></span> <span class="caret"></span></a>
<a href="#" class="dropdown-toggle hidden-sm hidden-md hidden-lg" data-toggle="dropdown">Settings <span class="domain-settings-badge badge"></span> <span class="caret"></span></a>
<ul id="domain-settings-nav-dropdown" class="dropdown-menu" role="menu">
</ul>
</li>
<li><a href="/content/">Content</a></li>
<li><a href="/settings/">Settings</a></li>
</ul>
<ul class="nav navbar-right navbar-nav">
<li><a id="visit-domain-link" class="blue-link" target="_blank" style="display: none;">Visit domain in VR</a></li>
<li><a href="#" id="restart-server"><span class="glyphicon glyphicon-refresh"></span> Restart</a></li>
<a id="visit-domain-link" class="btn btn-default navbar-btn" role="button" target="_blank" style="display: none;">
<img id="visit-hmd-icon" src="/images/hmd-w-eyes.svg" alt="Head-mounted display" />
Visit in VR
</a>
<button id="save-settings-button" class="save-button btn btn-success navbar-btn hidden-xs" disabled="true"><span class="save-button-text">Save</span></button>
<a href="#" id="restart-server" class="navbar-btn btn btn-link"><span class="glyphicon glyphicon-refresh"></span> Restart</a>
</ul>
</div>
</div><!-- /.container-fluid -->
@ -50,7 +68,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">domain-server is restarting</h4>
<h4 class="modal-title">Domain Server is restarting</h4>
</div>
<div class="modal-body">
<h5>This page will automatically refresh in <span id="refresh-time">3 seconds</span>.</h5>

View file

@ -0,0 +1,13 @@
<?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 456 244" style="enable-background:new 0 0 456 244;" xml:space="preserve">
<style type="text/css">
.st0{fill:#666666;}
</style>
<path class="st0" d="M352.8,3.4h-250C49.6,3.4,6.3,46.2,6.3,98.9v44.9c0,52.7,43.3,95.5,96.5,95.5h67.6c19.4,0,31.9-17.9,42.9-33.6
c4.2-6.1,11.1-15.9,14.8-17.9c3.6,2.1,10.4,12.2,14.5,18.4c10.4,15.5,22.1,33.1,40.3,33.1h69.9c53.3,0,96.5-42.9,96.5-95.5V98.9
C449.3,46.2,406,3.4,352.8,3.4z M129.6,157.6c-22.4,0-40.6-18.2-40.6-40.6s18.2-40.6,40.6-40.6c22.4,0,40.6,18.2,40.6,40.6
S151.9,157.6,129.6,157.6z M328.4,157.6c-22.4,0-40.6-18.2-40.6-40.6s18.2-40.6,40.6-40.6c22.4,0,40.6,18.2,40.6,40.6
S350.8,157.6,328.4,157.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 928 B

File diff suppressed because it is too large Load diff

View file

@ -23,26 +23,108 @@ function showRestartModal() {
}, 1000);
}
function settingsGroupAnchor(base, html_id) {
return base + "#" + html_id + "_group"
}
$(document).ready(function(){
var url = window.location;
var url = location.protocol + '//' + location.host+location.pathname;
// Will only work if string in href matches with location
$('ul.nav a[href="'+ url +'"]').parent().addClass('active');
// Will also work for relative and absolute hrefs
$('ul.nav a').filter(function() {
return this.href == url;
}).parent().addClass('active');
$('body').on('click', '#restart-server', function(e) {
swal( {
title: "Are you sure?",
text: "This will restart your domain server, causing your domain to be briefly offline.",
type: "warning",
html: true,
showCancelButton: true
}, function() {
$.get("/restart");
showRestartModal();
});
}).parent().addClass('active');
$('body').on('click', '#restart-server', function(e) {
swalAreYouSure(
"This will restart your domain server, causing your domain to be briefly offline.",
"Restart",
function() {
swal.close();
$.get("/restart");
showRestartModal();
}
)
return false;
});
var $contentDropdown = $('#content-settings-nav-dropdown');
var $settingsDropdown = $('#domain-settings-nav-dropdown');
// define extra groups to add to setting panels, with their splice index
Settings.extraContentGroupsAtIndex = {
0: {
html_id: Settings.CONTENT_ARCHIVES_PANEL_ID,
label: 'Content Archives'
},
1: {
html_id: Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID,
label: 'Upload Content'
}
};
Settings.extraContentGroupsAtEnd = [];
Settings.extraDomainGroupsAtIndex = {
1: {
html_id: 'places',
label: 'Places'
}
}
Settings.extraDomainGroupsAtEnd = [
{
html_id: 'settings_backup',
label: 'Settings Backup / Restore'
}
]
// for pages that have the settings dropdowns
if ($contentDropdown.length && $settingsDropdown.length) {
// make a JSON request to get the dropdown menus for content and settings
// we don't error handle here because the top level menu is still clickable and usables if this fails
$.getJSON('/settings-menu-groups.json', function(data){
function makeGroupDropdownElement(group, base) {
var html_id = group.html_id ? group.html_id : group.name;
return "<li class='setting-group'><a href='" + settingsGroupAnchor(base, html_id) + "'>" + group.label + "<span class='badge'></span></a></li>";
}
// add the dummy settings groups that get populated via JS
for (var spliceIndex in Settings.extraContentGroupsAtIndex) {
data.content_settings.splice(spliceIndex, 0, Settings.extraContentGroupsAtIndex[spliceIndex]);
}
for (var endIndex in Settings.extraContentGroupsAtEnd) {
data.content_settings.push(Settings.extraContentGroupsAtEnd[endIndex]);
}
$.each(data.content_settings, function(index, group){
if (index > 0) {
$contentDropdown.append("<li role='separator' class='divider'></li>");
}
$contentDropdown.append(makeGroupDropdownElement(group, "/content/"));
});
// add the dummy settings groups that get populated via JS
for (var spliceIndex in Settings.extraDomainGroupsAtIndex) {
data.domain_settings.splice(spliceIndex, 0, Settings.extraDomainGroupsAtIndex[spliceIndex]);
}
for (var endIndex in Settings.extraDomainGroupsAtEnd) {
data.domain_settings.push(Settings.extraDomainGroupsAtEnd[endIndex]);
}
$.each(data.domain_settings, function(index, group){
if (index > 0) {
$settingsDropdown.append("<li role='separator' class='divider'></li>");
}
$settingsDropdown.append(makeGroupDropdownElement(group, "/settings/"));
});
});
}
});

View file

@ -1,6 +1,8 @@
var Settings = {
showAdvanced: false,
ADVANCED_CLASS: 'advanced-setting',
if (typeof Settings === "undefined") {
Settings = {};
}
Object.assign(Settings, {
DEPRECATED_CLASS: 'deprecated-setting',
TRIGGER_CHANGE_CLASS: 'trigger-change',
DATA_ROW_CLASS: 'value-row',
@ -40,8 +42,10 @@ var Settings = {
ADD_PLACE_BTN_ID: 'add-place-btn',
FORM_ID: 'settings-form',
INVALID_ROW_CLASS: 'invalid-input',
DATA_ROW_INDEX: 'data-row-index'
};
DATA_ROW_INDEX: 'data-row-index',
CONTENT_ARCHIVES_PANEL_ID: 'content_archives',
UPLOAD_CONTENT_BACKUP_PANEL_ID: 'upload_content'
});
var URLs = {
// STABLE METAVERSE_URL: https://metaverse.highfidelity.com
@ -94,8 +98,25 @@ var DOMAIN_ID_TYPE_TEMP = 1;
var DOMAIN_ID_TYPE_FULL = 2;
var DOMAIN_ID_TYPE_UNKNOWN = 3;
function swalAreYouSure(text, confirmButtonText, callback) {
swal({
title: "Are you sure?",
text: text,
type: "warning",
showCancelButton: true,
confirmButtonText: confirmButtonText,
closeOnConfirm: false
}, callback);
}
function domainIDIsSet() {
return Settings.data.values.metaverse.id.length > 0;
if (typeof Settings.data.values.metaverse !== 'undefined' &&
typeof Settings.data.values.metaverse.id !== 'undefined') {
return Settings.data.values.metaverse.id.length > 0;
} else {
return false;
}
}
function getCurrentDomainIDType() {
@ -157,10 +178,11 @@ function getDomainFromAPI(callback) {
callback = function() {};
}
var domainID = Settings.data.values.metaverse.id;
if (domainID === null || domainID === undefined || domainID === '') {
if (!domainIDIsSet()) {
callback({ status: 'fail' });
return null;
} else {
var domainID = Settings.data.values.metaverse.id;
}
pendingDomainRequest = $.ajax({

File diff suppressed because one or more lines are too long

View file

@ -1,104 +1,29 @@
<!--#include virtual="header.html"-->
<div class="col-md-10 col-md-offset-1">
<div class="row">
<div class="col-md-12">
<div class="alert" style="display:none;"></div>
</div>
</div>
<script type="text/javascript">
var Settings = {
content_settings: false,
endpoint: "/settings.json",
path: "/settings/"
};
</script>
<div class="row">
<div class="col-md-3 col-sm-3" id="setup-sidebar-col">
<div id="setup-sidebar" data-clampedwidth="#setup-sidebar-col">
<script id="list-group-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% if (!group.hidden) { %>
<% panelID = group.name ? group.name : group.html_id %>
<li>
<a href="#<%- panelID %>" class="list-group-item">
<span class="badge"></span>
<%- group.label %>
</a>
</li>
<% } %>
<% }); %>
</script>
<ul class="nav nav-pills nav-stacked">
</ul>
<button id="advanced-toggle-button" class="btn btn-info advanced-toggle">Show advanced</button>
<button class="btn btn-success save-button" disabled>Save</button>
<div id="manage-cloud-domains-link" style="display: none;">
<a href="https://highfidelity.com/user/cloud_domains" target="_blank" class="blue-link">Manage Cloud Hosted Domains</a>
</div>
</div>
</div>
<div class="col-md-9 col-sm-9 col-xs-12">
<div class="col-md-10 col-md-offset-1 col-xs-12">
<div class="row">
<div class="col-xs-12">
<div id="cloud-domains-alert" class="alert alert-info alert-dismissible" role="alert" style="display: none;">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span class="alert-link">
<a href="https://highfidelity.com/user/cloud_domains" target="_blank" class="blue-link">Visit Cloud Hosted Domains</a> to manage all your cloud domains
</span>
</div>
<form id="settings-form" role="form">
<script id="panels-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% if (!group.hidden) { %>
<% var settings = _.partition(group.settings, function(value, index) { return !value.deprecated })[0] %>
<% split_settings = _.partition(settings, function(value, index) { return !value.advanced }) %>
<% isAdvanced = _.isEmpty(split_settings[0]) && !_.isEmpty(split_settings[1]) %>
<% if (isAdvanced) { %>
<% $("a[href=#" + group.name + "]").addClass('advanced-setting').hide() %>
<% } %>
<% isGrouped = !!group.name %>
<% panelID = isGrouped ? group.name : group.html_id %>
<div class="panel panel-default<%- (isAdvanced) ? ' advanced-setting' : '' %><%- (isGrouped) ? ' grouped' : '' %>"
id="<%- panelID %>">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
</div>
<div class="panel-body">
<% _.each(split_settings[0], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, false) %>
<% }); %>
<% if (!_.isEmpty(split_settings[1])) { %>
<% $("#advanced-toggle-button").show() %>
<% _.each(split_settings[1], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, true) %>
<% }); %>
<% }%>
</div>
</div>
<% } %>
<% }); %>
</script>
<div id="panels"></div>
</form>
</div>
</div>
<div class="col-xs-12 hidden-sm hidden-md hidden-lg">
<button class="btn btn-success save-button" id="small-save-button">Save</button>
</div>
<div class="alert alert-info">Your domain content settings are now available in <a href='/content/'>Content</a></div>
</div>
</div>
</div>
<!--#include virtual="base-settings.html"-->
<!--#include virtual="footer.html"-->
<script src='/js/underscore-min.js'></script>
<script src='/js/underscore-keypath.min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='/js/sha256.js'></script>
<script src='js/form2js.min.js'></script>
<script src='js/bootstrap-switch.min.js'></script>
<script src='/js/shared.js'></script>
<script src='js/settings.js'></script>
<!--#include virtual="base-settings-scripts.html"-->
<script src="js/settings.js"></script>
<!--#include virtual="page-end.html"-->

File diff suppressed because it is too large Load diff

View file

@ -261,6 +261,5 @@
<script src='/js/underscore-min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='/js/sha256.js'></script>
<script src='/js/shared.js'></script>
<script src='js/wizard.js'></script>
<!--#include virtual="page-end.html"-->

View file

@ -396,10 +396,12 @@ function savePermissions() {
var admins = $('#admin-usernames').val().split(',');
var existingAdmins = Settings.data.values.security.permissions.map(function(value) {
return value.permissions_id;
});
admins = admins.concat(existingAdmins);
if (Settings.data.values.security.permissions) {
var existingAdmins = Settings.data.values.security.permissions.map(function(value) {
return value.permissions_id;
});
admins = admins.concat(existingAdmins);
}
// Filter out unique values
admins = _.uniq(admins.map(function(username) {

View file

@ -0,0 +1,605 @@
//
// AssetsBackupHandler.cpp
// domain-server/src
//
// Created by Clement Brisset on 1/12/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
//
#include "AssetsBackupHandler.h"
#include <QJsonDocument>
#include <QDate>
#include <QtCore/QLoggingCategory>
#include <quazip5/quazipfile.h>
#include <quazip5/quazipdir.h>
#include <AssetClient.h>
#include <AssetRequest.h>
#include <AssetUpload.h>
#include <MappingRequest.h>
#include <PathUtils.h>
using namespace std;
static const QString ASSETS_DIR { "/assets/" };
static const QString MAPPINGS_FILE { "mappings.json" };
static const QString ZIP_ASSETS_FOLDER { "files" };
static const chrono::minutes MAX_REFRESH_TIME { 5 };
Q_DECLARE_LOGGING_CATEGORY(asset_backup)
Q_LOGGING_CATEGORY(asset_backup, "hifi.asset-backup");
AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) :
_assetsDirectory(backupDirectory + ASSETS_DIR)
{
// Make sure the asset directory exists.
QDir(_assetsDirectory).mkpath(".");
refreshAssetsOnDisk();
setupRefreshTimer();
}
void AssetsBackupHandler::setupRefreshTimer() {
_mappingsRefreshTimer.setTimerType(Qt::CoarseTimer);
_mappingsRefreshTimer.setSingleShot(true);
QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &AssetsBackupHandler::refreshMappings);
auto nodeList = DependencyManager::get<LimitedNodeList>();
QObject::connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, [this](SharedNodePointer node) {
if (node->getType() == NodeType::AssetServer) {
// run immediately for the first time.
_mappingsRefreshTimer.start(0);
}
});
QObject::connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, [this](SharedNodePointer node) {
if (node->getType() == NodeType::AssetServer) {
_mappingsRefreshTimer.stop();
}
});
}
void AssetsBackupHandler::refreshAssetsOnDisk() {
QDir assetsDir { _assetsDirectory };
auto assetNames = assetsDir.entryList(QDir::Files);
// store all valid hashes
copy_if(begin(assetNames), end(assetNames),
inserter(_assetsOnDisk, begin(_assetsOnDisk)),
AssetUtils::isValidHash);
}
void AssetsBackupHandler::refreshAssetsInBackups() {
_assetsInBackups.clear();
for (const auto& backup : _backups) {
for (const auto& mapping : backup.mappings) {
_assetsInBackups.insert(mapping.second);
}
}
}
void AssetsBackupHandler::checkForMissingAssets() {
vector<AssetUtils::AssetHash> missingAssets;
set_difference(begin(_assetsInBackups), end(_assetsInBackups),
begin(_assetsOnDisk), end(_assetsOnDisk),
back_inserter(missingAssets));
if (missingAssets.size() > 0) {
qCWarning(asset_backup) << "Found" << missingAssets.size() << "backup assets missing from disk.";
}
}
void AssetsBackupHandler::checkForAssetsToDelete() {
vector<AssetUtils::AssetHash> deprecatedAssets;
set_difference(begin(_assetsOnDisk), end(_assetsOnDisk),
begin(_assetsInBackups), end(_assetsInBackups),
back_inserter(deprecatedAssets));
if (deprecatedAssets.size() > 0) {
qCDebug(asset_backup) << "Found" << deprecatedAssets.size() << "backup assets to delete from disk.";
const auto noCorruptedBackups = none_of(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) {
return backup.corruptedBackup;
});
if (noCorruptedBackups) {
for (const auto& hash : deprecatedAssets) {
auto success = QFile::remove(_assetsDirectory + hash);
if (success) {
_assetsOnDisk.erase(hash);
} else {
qCWarning(asset_backup) << "Could not delete asset:" << hash;
}
}
} else {
qCWarning(asset_backup) << "Some backups did not load properly, aborting delete operation for safety.";
}
}
}
bool AssetsBackupHandler::isCorruptedBackup(const QString& backupName) {
auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& value) {
return value.name == backupName;
});
if (it == end(_backups)) {
return false;
}
return it->corruptedBackup;
}
std::pair<bool, float> AssetsBackupHandler::isAvailable(const QString& backupName) {
const auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) {
return backup.name == backupName;
});
if (it == end(_backups)) {
return { true, 1.0f };
}
int mappingsMissing = 0;
for (const auto& mapping : it->mappings) {
if (_assetsLeftToRequest.find(mapping.second) != end(_assetsLeftToRequest)) {
++mappingsMissing;
}
}
if (mappingsMissing == 0) {
return { true, 1.0f };
}
float progress = (float)it->mappings.size();
progress -= (float)mappingsMissing;
progress /= it->mappings.size();
return { false, progress };
}
std::pair<bool, float> AssetsBackupHandler::getRecoveryStatus() {
if (_assetsLeftToUpload.empty() &&
_mappingsLeftToSet.empty() &&
_mappingsLeftToDelete.empty() &&
_mappingRequestsInFlight == 0) {
return { false, 1.0f };
}
float progress = (float)_numRestoreOperations;
progress -= (float)_assetsLeftToUpload.size();
progress -= (float)_mappingRequestsInFlight;
progress /= (float)_numRestoreOperations;
return { true, progress };
}
void AssetsBackupHandler::loadBackup(const QString& backupName, QuaZip& zip) {
Q_ASSERT(QThread::currentThread() == thread());
_backups.emplace_back(backupName, AssetUtils::Mappings(), false);
auto& backup = _backups.back();
if (!zip.setCurrentFile(MAPPINGS_FILE)) {
qCCritical(asset_backup) << "Failed to find" << MAPPINGS_FILE << "while loading backup";
qCCritical(asset_backup) << " Error:" << zip.getZipError();
backup.corruptedBackup = true;
return;
}
QuaZipFile zipFile { &zip };
if (!zipFile.open(QFile::ReadOnly)) {
qCCritical(asset_backup) << "Could not unzip backup file for load:" << MAPPINGS_FILE;
qCCritical(asset_backup) << " Error:" << zip.getZipError();
backup.corruptedBackup = true;
return;
}
QJsonParseError error;
auto document = QJsonDocument::fromJson(zipFile.readAll(), &error);
if (document.isNull() || !document.isObject()) {
qCCritical(asset_backup) << "Could not parse backup file to JSON object for load:" << MAPPINGS_FILE;
qCCritical(asset_backup) << " Error:" << error.errorString();
backup.corruptedBackup = true;
return;
}
auto jsonObject = document.object();
for (auto it = begin(jsonObject); it != end(jsonObject); ++it) {
const auto& assetPath = it.key();
const auto& assetHash = it.value().toString();
if (!AssetUtils::isValidHash(assetHash)) {
qCCritical(asset_backup) << "Corrupted mapping in loading backup file" << backupName << ":" << it.key();
backup.corruptedBackup = true;
continue;
}
backup.mappings[assetPath] = assetHash;
_assetsInBackups.insert(assetHash);
}
}
void AssetsBackupHandler::loadingComplete() {
checkForMissingAssets();
checkForAssetsToDelete();
}
void AssetsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) {
Q_ASSERT(QThread::currentThread() == thread());
if (operationInProgress()) {
qCWarning(asset_backup) << "There is already an operation in progress.";
return;
}
if (_lastMappingsRefresh.time_since_epoch().count() == 0) {
qCWarning(asset_backup) << "Current mappings not yet loaded.";
return;
}
if ((p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) {
qCWarning(asset_backup) << "Backing up asset mappings that might be stale.";
}
AssetUtils::Mappings mappings;
QJsonObject jsonObject;
for (const auto& mapping : _currentMappings) {
mappings[mapping.first] = mapping.second;
_assetsInBackups.insert(mapping.second);
jsonObject.insert(mapping.first, mapping.second);
}
QJsonDocument document(jsonObject);
QuaZipFile zipFile { &zip };
if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(MAPPINGS_FILE))) {
qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError();
return;
}
zipFile.write(document.toJson());
zipFile.close();
if (zipFile.getZipError() != UNZ_OK) {
qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError();
return;
}
_backups.emplace_back(backupName, mappings, false);
qDebug() << "Created asset backup:" << backupName;
}
void AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) {
Q_ASSERT(QThread::currentThread() == thread());
if (operationInProgress()) {
qCWarning(asset_backup) << "There is already a backup/restore in progress.";
return;
}
if (_lastMappingsRefresh.time_since_epoch().count() == 0) {
qCWarning(asset_backup) << "Current mappings not yet loaded.";
return;
}
if ((p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) {
qCWarning(asset_backup) << "Recovering while current asset mappings might be stale.";
}
auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) {
return backup.name == backupName;
});
if (it == end(_backups)) {
loadBackup(backupName, zip);
QuaZipDir zipDir { &zip, ZIP_ASSETS_FOLDER };
auto assetNames = zipDir.entryList(QDir::Files);
for (const auto& asset : assetNames) {
if (AssetUtils::isValidHash(asset)) {
if (!zip.setCurrentFile(zipDir.filePath(asset))) {
qCCritical(asset_backup) << "Failed to find" << asset << "while recovering backup";
qCCritical(asset_backup) << " Error:" << zip.getZipError();
continue;
}
QuaZipFile zipFile { &zip };
if (!zipFile.open(QFile::ReadOnly)) {
qCCritical(asset_backup) << "Could not unzip asset file:" << asset;
qCCritical(asset_backup) << " Error:" << zip.getZipError();
continue;
}
writeAssetFile(asset, zipFile.readAll());
}
}
// iterator is end() and has been invalidated in the `loadBackup` call
// grab the new iterator
it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) {
return backup.name == backupName;
});
if (it == end(_backups)) {
qCCritical(asset_backup) << "Failed to recover backup:" << backupName;
return;
}
}
const auto& newMappings = it->mappings;
computeServerStateDifference(_currentMappings, newMappings);
restoreAllAssets();
}
void AssetsBackupHandler::deleteBackup(const QString& backupName) {
Q_ASSERT(QThread::currentThread() == thread());
if (operationInProgress()) {
qCWarning(asset_backup) << "There is a backup/restore in progress.";
return;
}
const auto it = remove_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) {
return backup.name == backupName;
});
if (it == end(_backups)) {
qCDebug(asset_backup) << "Could not find backup" << backupName << "to delete.";
return;
}
_backups.erase(it, end(_backups));
refreshAssetsInBackups();
checkForAssetsToDelete();
qDebug() << "Deleted asset backup:" << backupName;
}
void AssetsBackupHandler::consolidateBackup(const QString& backupName, QuaZip& zip) {
Q_ASSERT(QThread::currentThread() == thread());
if (operationInProgress()) {
qCWarning(asset_backup) << "There is a backup/restore in progress.";
return;
}
const auto it = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) {
return backup.name == backupName;
});
if (it == end(_backups)) {
qCDebug(asset_backup) << "Could not find backup" << backupName << "to consolidate.";
return;
}
for (const auto& mapping : it->mappings) {
const auto& hash = mapping.second;
QDir assetsDir { _assetsDirectory };
QFile file { assetsDir.filePath(hash) };
if (!file.open(QFile::ReadOnly)) {
qCCritical(asset_backup) << "Could not open asset file" << file.fileName();
continue;
}
QuaZipFile zipFile { &zip };
if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + "/" + hash))) {
qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError();
continue;
}
zipFile.write(file.readAll());
zipFile.close();
if (zipFile.getZipError() != UNZ_OK) {
qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError();
continue;
}
}
}
void AssetsBackupHandler::refreshMappings() {
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createGetAllMappingsRequest();
QObject::connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) {
if (request->getError() == MappingRequest::NoError) {
const auto& mappings = request->getMappings();
// Clear existing mappings
_currentMappings.clear();
// Set new mapping, but ignore baked assets
for (const auto& mapping : mappings) {
if (!mapping.first.startsWith(AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER)) {
_currentMappings.insert({ mapping.first, mapping.second.hash });
}
}
_lastMappingsRefresh = p_high_resolution_clock::now();
downloadMissingFiles(_currentMappings);
} else {
qCCritical(asset_backup) << "Could not refresh asset server mappings.";
qCCritical(asset_backup) << " Error:" << request->getErrorString();
}
request->deleteLater();
// Launch next mappings request
static constexpr int MAPPINGS_REFRESH_INTERVAL = 30 * 1000;
_mappingsRefreshTimer.start(MAPPINGS_REFRESH_INTERVAL);
});
request->start();
}
void AssetsBackupHandler::downloadMissingFiles(const AssetUtils::Mappings& mappings) {
auto wasEmpty = _assetsLeftToRequest.empty();
for (const auto& mapping : mappings) {
const auto& hash = mapping.second;
if (_assetsOnDisk.find(hash) == end(_assetsOnDisk)) {
_assetsLeftToRequest.insert(hash);
}
}
// If we were empty, that means no download chain was already going, start one.
if (wasEmpty) {
downloadNextMissingFile();
}
}
void AssetsBackupHandler::downloadNextMissingFile() {
if (_assetsLeftToRequest.empty()) {
return;
}
auto hash = *begin(_assetsLeftToRequest);
auto assetClient = DependencyManager::get<AssetClient>();
auto assetRequest = assetClient->createRequest(hash);
QObject::connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) {
if (request->getError() == AssetRequest::NoError) {
qCDebug(asset_backup) << "Backing up asset" << request->getHash();
bool success = writeAssetFile(request->getHash(), request->getData());
if (!success) {
qCCritical(asset_backup) << "Failed to write asset file" << request->getHash();
}
} else {
qCCritical(asset_backup) << "Failed to backup asset" << request->getHash();
}
_assetsLeftToRequest.erase(request->getHash());
downloadNextMissingFile();
request->deleteLater();
});
assetRequest->start();
}
bool AssetsBackupHandler::writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data) {
QDir assetsDir { _assetsDirectory };
QFile file { assetsDir.filePath(hash) };
if (!file.open(QFile::WriteOnly)) {
qCCritical(asset_backup) << "Could not open asset file for write:" << file.fileName();
return false;
}
auto bytesWritten = file.write(data);
if (bytesWritten != data.size()) {
qCCritical(asset_backup) << "Could not write data to file" << file.fileName();
file.remove();
return false;
}
_assetsOnDisk.insert(hash);
return true;
}
void AssetsBackupHandler::computeServerStateDifference(const AssetUtils::Mappings& currentMappings,
const AssetUtils::Mappings& newMappings) {
_mappingsLeftToSet.reserve((int)newMappings.size());
_assetsLeftToUpload.reserve((int)newMappings.size());
_mappingsLeftToDelete.reserve((int)currentMappings.size());
set<AssetUtils::AssetHash> currentAssets;
for (const auto& currentMapping : currentMappings) {
const auto& currentPath = currentMapping.first;
const auto& currentHash = currentMapping.second;
if (newMappings.find(currentPath) == end(newMappings)) {
_mappingsLeftToDelete.push_back(currentPath);
}
currentAssets.insert(currentHash);
}
for (const auto& newMapping : newMappings) {
const auto& newPath = newMapping.first;
const auto& newHash = newMapping.second;
auto it = currentMappings.find(newPath);
if (it == end(currentMappings) || it->second != newHash) {
_mappingsLeftToSet.push_back({ newPath, newHash });
}
if (currentAssets.find(newHash) == end(currentAssets)) {
_assetsLeftToUpload.push_back(newHash);
}
}
_numRestoreOperations = (int)_assetsLeftToUpload.size() + (int)_mappingsLeftToSet.size();
if (!_mappingsLeftToDelete.empty()) {
++_numRestoreOperations;
}
qCDebug(asset_backup) << "Mappings to set:" << _mappingsLeftToSet.size();
qCDebug(asset_backup) << "Mappings to del:" << _mappingsLeftToDelete.size();
qCDebug(asset_backup) << "Assets to upload:" << _assetsLeftToUpload.size();
}
void AssetsBackupHandler::restoreAllAssets() {
restoreNextAsset();
}
void AssetsBackupHandler::restoreNextAsset() {
if (_assetsLeftToUpload.empty()) {
updateMappings();
return;
}
auto hash = _assetsLeftToUpload.back();
_assetsLeftToUpload.pop_back();
auto assetFilename = _assetsDirectory + hash;
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createUpload(assetFilename);
QObject::connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) {
if (request->getError() != AssetUpload::NoError) {
qCCritical(asset_backup) << "Failed to restore asset:" << request->getFilename();
qCCritical(asset_backup) << " Error:" << request->getErrorString();
}
restoreNextAsset();
request->deleteLater();
});
request->start();
}
void AssetsBackupHandler::updateMappings() {
auto assetClient = DependencyManager::get<AssetClient>();
for (const auto& mapping : _mappingsLeftToSet) {
auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second);
QObject::connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) {
if (request->getError() != MappingRequest::NoError) {
qCCritical(asset_backup) << "Failed to set mapping:" << request->getPath();
qCCritical(asset_backup) << " Error:" << request->getErrorString();
}
--_mappingRequestsInFlight;
request->deleteLater();
});
request->start();
++_mappingRequestsInFlight;
}
_mappingsLeftToSet.clear();
auto request = assetClient->createDeleteMappingsRequest(_mappingsLeftToDelete);
QObject::connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) {
if (request->getError() != MappingRequest::NoError) {
qCCritical(asset_backup) << "Failed to delete mappings";
qCCritical(asset_backup) << " Error:" << request->getErrorString();
}
--_mappingRequestsInFlight;
request->deleteLater();
});
_mappingsLeftToDelete.clear();
request->start();
++_mappingRequestsInFlight;
}

View file

@ -0,0 +1,98 @@
//
// AssetsBackupHandler.h
// domain-server/src
//
// Created by Clement Brisset on 1/12/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
//
#ifndef hifi_AssetsBackupHandler_h
#define hifi_AssetsBackupHandler_h
#include <set>
#include <map>
#include <QObject>
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>
#include <AssetUtils.h>
#include <ReceivedMessage.h>
#include <PortableHighResolutionClock.h>
#include "BackupHandler.h"
class AssetsBackupHandler : public QObject, public BackupHandlerInterface {
Q_OBJECT
public:
AssetsBackupHandler(const QString& backupDirectory);
std::pair<bool, float> isAvailable(const QString& backupName) override;
std::pair<bool, float> getRecoveryStatus() override;
void loadBackup(const QString& backupName, QuaZip& zip) override;
void loadingComplete() override;
void createBackup(const QString& backupName, QuaZip& zip) override;
void recoverBackup(const QString& backupName, QuaZip& zip) override;
void deleteBackup(const QString& backupName) override;
void consolidateBackup(const QString& backupName, QuaZip& zip) override;
bool isCorruptedBackup(const QString& backupName) override;
bool operationInProgress() { return getRecoveryStatus().first; }
private:
void setupRefreshTimer();
void refreshMappings();
void refreshAssetsInBackups();
void refreshAssetsOnDisk();
void checkForMissingAssets();
void checkForAssetsToDelete();
void downloadMissingFiles(const AssetUtils::Mappings& mappings);
void downloadNextMissingFile();
bool writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data);
void computeServerStateDifference(const AssetUtils::Mappings& currentMappings,
const AssetUtils::Mappings& newMappings);
void restoreAllAssets();
void restoreNextAsset();
void updateMappings();
QString _assetsDirectory;
QTimer _mappingsRefreshTimer;
p_high_resolution_clock::time_point _lastMappingsRefresh;
AssetUtils::Mappings _currentMappings;
struct AssetServerBackup {
AssetServerBackup(const QString& pName, AssetUtils::Mappings pMappings, bool pCorruptedBackup) :
name(pName), mappings(pMappings), corruptedBackup(pCorruptedBackup) {}
QString name;
AssetUtils::Mappings mappings;
bool corruptedBackup;
};
// Internal storage for backups on disk
std::vector<AssetServerBackup> _backups;
std::set<AssetUtils::AssetHash> _assetsInBackups;
std::set<AssetUtils::AssetHash> _assetsOnDisk;
// Internal storage for backup in progress
std::set<AssetUtils::AssetHash> _assetsLeftToRequest;
// Internal storage for restore in progress
std::vector<AssetUtils::AssetHash> _assetsLeftToUpload;
std::vector<std::pair<AssetUtils::AssetPath, AssetUtils::AssetHash>> _mappingsLeftToSet;
AssetUtils::AssetPathList _mappingsLeftToDelete;
int _mappingRequestsInFlight { 0 };
int _numRestoreOperations { 0 }; // Used to compute a restore progress.
};
#endif /* hifi_AssetsBackupHandler_h */

View file

@ -0,0 +1,40 @@
//
// BackupHandler.h
// domain-server/src
//
// Created by Clement Brisset on 2/5/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
//
#ifndef hifi_BackupHandler_h
#define hifi_BackupHandler_h
#include <memory>
#include <QString>
class QuaZip;
class BackupHandlerInterface {
public:
virtual ~BackupHandlerInterface() = default;
virtual std::pair<bool, float> isAvailable(const QString& backupName) = 0;
// Returns whether a recovery is ongoing and a progress between 0 and 1 if one is.
virtual std::pair<bool, float> getRecoveryStatus() = 0;
virtual void loadBackup(const QString& backupName, QuaZip& zip) = 0;
virtual void loadingComplete() = 0;
virtual void createBackup(const QString& backupName, QuaZip& zip) = 0;
virtual void recoverBackup(const QString& backupName, QuaZip& zip) = 0;
virtual void deleteBackup(const QString& backupName) = 0;
virtual void consolidateBackup(const QString& backupName, QuaZip& zip) = 0;
virtual bool isCorruptedBackup(const QString& backupName) = 0;
};
using BackupHandlerPointer = std::unique_ptr<BackupHandlerInterface>;
#endif /* hifi_BackupHandler_h */

View file

@ -0,0 +1,74 @@
//
// ContentSettingsBackupHandler.cpp
// domain-server/src
//
// Created by Stephen Birarda on 2/15/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
//
#include "ContentSettingsBackupHandler.h"
#include <quazip5/quazip.h>
#include <quazip5/quazipfile.h>
ContentSettingsBackupHandler::ContentSettingsBackupHandler(DomainServerSettingsManager& domainServerSettingsManager) :
_settingsManager(domainServerSettingsManager)
{
}
static const QString CONTENT_SETTINGS_BACKUP_FILENAME = "content-settings.json";
void ContentSettingsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) {
// grab the content settings as JSON, excluding default values and values hidden from backup
QJsonObject contentSettingsJSON = _settingsManager.settingsResponseObjectForType(
"", // include all settings types
DomainServerSettingsManager::Authenticated, DomainServerSettingsManager::NoDomainSettings,
DomainServerSettingsManager::IncludeContentSettings, DomainServerSettingsManager::NoDefaultSettings,
DomainServerSettingsManager::ForBackup
);
// make a QJsonDocument using the object
QJsonDocument contentSettingsDocument { contentSettingsJSON };
QuaZipFile zipFile { &zip };
if (zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(CONTENT_SETTINGS_BACKUP_FILENAME))) {
if (zipFile.write(contentSettingsDocument.toJson()) == -1) {
qCritical().nospace() << "Failed to write to " << CONTENT_SETTINGS_BACKUP_FILENAME << ": " << zipFile.getZipError();
}
zipFile.close();
if (zipFile.getZipError() != UNZ_OK) {
qCritical().nospace() << "Failed to zip " << CONTENT_SETTINGS_BACKUP_FILENAME << ": " << zipFile.getZipError();
}
} else {
qCritical().nospace() << "Failed to open " << CONTENT_SETTINGS_BACKUP_FILENAME << ": " << zipFile.getZipError();
}
}
void ContentSettingsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) {
if (!zip.setCurrentFile(CONTENT_SETTINGS_BACKUP_FILENAME)) {
qWarning() << "Failed to find" << CONTENT_SETTINGS_BACKUP_FILENAME << "while recovering backup";
return;
}
QuaZipFile zipFile { &zip };
if (!zipFile.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open" << CONTENT_SETTINGS_BACKUP_FILENAME << "in backup";
return;
}
auto rawData = zipFile.readAll();
zipFile.close();
QJsonDocument jsonDocument = QJsonDocument::fromJson(rawData);
if (!_settingsManager.restoreSettingsFromObject(jsonDocument.object(), ContentSettings)) {
qCritical() << "Failed to restore settings from" << CONTENT_SETTINGS_BACKUP_FILENAME << "in content archive";
}
}

View file

@ -0,0 +1,43 @@
//
// ContentSettingsBackupHandler.h
// domain-server/src
//
// Created by Stephen Birarda on 2/15/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
//
#ifndef hifi_ContentSettingsBackupHandler_h
#define hifi_ContentSettingsBackupHandler_h
#include "BackupHandler.h"
#include "DomainServerSettingsManager.h"
class ContentSettingsBackupHandler : public BackupHandlerInterface {
public:
ContentSettingsBackupHandler(DomainServerSettingsManager& domainServerSettingsManager);
std::pair<bool, float> isAvailable(const QString& backupName) override { return { true, 1.0f }; }
std::pair<bool, float> getRecoveryStatus() override { return { false, 1.0f }; }
void loadBackup(const QString& backupName, QuaZip& zip) override {}
void loadingComplete() override {}
void createBackup(const QString& backupName, QuaZip& zip) override;
void recoverBackup(const QString& backupName, QuaZip& zip) override;
void deleteBackup(const QString& backupName) override {}
void consolidateBackup(const QString& backupName, QuaZip& zip) override {}
bool isCorruptedBackup(const QString& backupName) override { return false; }
private:
DomainServerSettingsManager& _settingsManager;
};
#endif // hifi_ContentSettingsBackupHandler_h

View file

@ -0,0 +1,599 @@
//
// DomainContentBackupManager.cpp
// libraries/domain-server/src
//
// Created by Ryan Huffman on 1/01/18.
// Adapted from OctreePersistThread
// 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 "DomainContentBackupManager.h"
#include <chrono>
#include <thread>
#include <cstdio>
#include <fstream>
#include <time.h>
#include <QBuffer>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <quazip5/quazip.h>
#include <NumericalConstants.h>
#include <PerfStat.h>
#include <PathUtils.h>
#include <shared/QtHelpers.h>
#include "DomainServer.h"
const std::chrono::seconds DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL { 30 };
// Backup format looks like: daily_backup-TIMESTAMP.zip
static const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" };
static const QString DATETIME_FORMAT_RE { "\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}" };
static const QString AUTOMATIC_BACKUP_PREFIX { "autobackup-" };
static const QString MANUAL_BACKUP_PREFIX { "backup-" };
static const QString MANUAL_BACKUP_NAME_RE { "[a-zA-Z0-9\\-_ ]+" };
void DomainContentBackupManager::addBackupHandler(BackupHandlerPointer handler) {
_backupHandlers.push_back(std::move(handler));
}
DomainContentBackupManager::DomainContentBackupManager(const QString& backupDirectory,
const QVariantList& backupRules,
std::chrono::milliseconds persistInterval,
bool debugTimestampNow) :
_backupDirectory(backupDirectory), _persistInterval(persistInterval), _lastCheck(p_high_resolution_clock::now())
{
setObjectName("DomainContentBackupManager");
// Make sure the backup directory exists.
QDir(_backupDirectory).mkpath(".");
parseBackupRules(backupRules);
}
void DomainContentBackupManager::parseBackupRules(const QVariantList& backupRules) {
qCDebug(domain_server) << "BACKUP RULES:";
for (const QVariant& value : backupRules) {
QVariantMap map = value.toMap();
int interval = map["backupInterval"].toInt();
int count = map["maxBackupVersions"].toInt();
auto name = map["Name"].toString();
auto format = name.toLower();
QRegExp matchDisallowedCharacters { "[^a-zA-Z0-9\\-_]+" };
format.replace(matchDisallowedCharacters, "_");
qCDebug(domain_server) << " Name:" << name;
qCDebug(domain_server) << " format:" << format;
qCDebug(domain_server) << " interval:" << interval;
qCDebug(domain_server) << " count:" << count;
BackupRule newRule = { name, interval, format, count, 0 };
newRule.lastBackupSeconds = getMostRecentBackupTimeInSecs(format);
if (newRule.lastBackupSeconds > 0) {
auto now = QDateTime::currentSecsSinceEpoch();
auto sinceLastBackup = now - newRule.lastBackupSeconds;
qCDebug(domain_server).noquote() << " lastBackup:" << formatSecTime(sinceLastBackup) << "ago";
} else {
qCDebug(domain_server) << " lastBackup: NEVER";
}
_backupRules.push_back(newRule);
}
}
void DomainContentBackupManager::refreshBackupRules() {
for (auto& backup : _backupRules) {
backup.lastBackupSeconds = getMostRecentBackupTimeInSecs(backup.extensionFormat);
}
}
int64_t DomainContentBackupManager::getMostRecentBackupTimeInSecs(const QString& format) {
int64_t mostRecentBackupInSecs = 0;
QString mostRecentBackupFileName;
QDateTime mostRecentBackupTime;
bool recentBackup = getMostRecentBackup(format, mostRecentBackupFileName, mostRecentBackupTime);
if (recentBackup) {
mostRecentBackupInSecs = mostRecentBackupTime.toSecsSinceEpoch();
}
return mostRecentBackupInSecs;
}
void DomainContentBackupManager::setup() {
auto backups = getAllBackups();
for (auto& backup : backups) {
QFile backupFile { backup.absolutePath };
if (!backupFile.open(QIODevice::ReadOnly)) {
qCritical() << "Could not open file:" << backup.absolutePath;
qCritical() << " ERROR:" << backupFile.errorString();
continue;
}
QuaZip zip { &backupFile };
if (!zip.open(QuaZip::mdUnzip)) {
qCritical() << "Could not open backup archive:" << backup.absolutePath;
qCritical() << " ERROR:" << zip.getZipError();
continue;
}
for (auto& handler : _backupHandlers) {
handler->loadBackup(backup.id, zip);
}
zip.close();
}
for (auto& handler : _backupHandlers) {
handler->loadingComplete();
}
}
bool DomainContentBackupManager::process() {
if (isStillRunning()) {
constexpr int64_t MSECS_TO_USECS = 1000;
constexpr int64_t USECS_TO_SLEEP = 10 * MSECS_TO_USECS; // every 10ms
std::this_thread::sleep_for(std::chrono::microseconds(USECS_TO_SLEEP));
if (_isRecovering) {
bool isStillRecovering = any_of(begin(_backupHandlers), end(_backupHandlers), [](const BackupHandlerPointer& handler) {
return handler->getRecoveryStatus().first;
});
if (!isStillRecovering) {
_isRecovering = false;
_recoveryFilename = "";
emit recoveryCompleted();
}
}
auto now = p_high_resolution_clock::now();
auto sinceLastSave = now - _lastCheck;
if (sinceLastSave > _persistInterval) {
_lastCheck = now;
if (!_isRecovering) {
backup();
}
}
}
return isStillRunning();
}
void DomainContentBackupManager::shutdown() {
// Destroy handlers on the correct thread so that they can cleanup timers
_backupHandlers.clear();
}
void DomainContentBackupManager::aboutToFinish() {
_stopThread = true;
}
bool DomainContentBackupManager::getMostRecentBackup(const QString& format,
QString& mostRecentBackupFileName,
QDateTime& mostRecentBackupTime) {
QRegExp formatRE { AUTOMATIC_BACKUP_PREFIX + QRegExp::escape(format) + "\\-(" + DATETIME_FORMAT_RE + ")" + "\\.zip" };
QStringList filters;
filters << AUTOMATIC_BACKUP_PREFIX + format + "*.zip";
bool bestBackupFound = false;
QString bestBackupFile;
QDateTime bestBackupFileTime;
// Iterate over all of the backup files in the persist location
QDirIterator dirIterator(_backupDirectory, filters, QDir::Files | QDir::NoSymLinks, QDirIterator::NoIteratorFlags);
while (dirIterator.hasNext()) {
dirIterator.next();
auto fileName = dirIterator.fileInfo().fileName();
if (formatRE.exactMatch(fileName)) {
auto datetime = formatRE.cap(1);
auto createdAt = QDateTime::fromString(datetime, DATETIME_FORMAT);
if (!createdAt.isValid()) {
qDebug() << "Skipping backup with invalid timestamp: " << datetime;
continue;
}
qDebug() << "Checking " << dirIterator.fileInfo().filePath();
// Based on last modified date, track the most recently modified file as the best backup
if (createdAt > bestBackupFileTime) {
bestBackupFound = true;
bestBackupFile = dirIterator.filePath();
bestBackupFileTime = createdAt;
}
} else {
qDebug() << "NO match: " << fileName << formatRE;
}
}
// If we found a backup then return the results
if (bestBackupFound) {
mostRecentBackupFileName = bestBackupFile;
mostRecentBackupTime = bestBackupFileTime;
}
return bestBackupFound;
}
void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, const QString& backupName) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "deleteBackup", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(const QString&, backupName));
return;
}
if (_isRecovering && backupName == _recoveryFilename) {
promise->resolve({
{ "success", false }
});
return;
}
QDir backupDir { _backupDirectory };
QFile backupFile { backupDir.filePath(backupName) };
auto success = backupFile.remove();
refreshBackupRules();
for (auto& handler : _backupHandlers) {
handler->deleteBackup(backupName);
}
promise->resolve({
{ "success", success }
});
}
bool DomainContentBackupManager::recoverFromBackupZip(const QString& backupName, QuaZip& zip) {
if (!zip.open(QuaZip::Mode::mdUnzip)) {
qWarning() << "Failed to unzip file: " << backupName;
return false;
} else {
_isRecovering = true;
_recoveryFilename = backupName;
for (auto& handler : _backupHandlers) {
handler->recoverBackup(backupName, zip);
}
qDebug() << "Successfully started recovering from " << backupName;
return true;
}
}
void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName) {
if (_isRecovering) {
promise->resolve({
{ "success", false }
});
return;
};
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "recoverFromBackup", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(const QString&, backupName));
return;
}
qDebug() << "Recovering from" << backupName;
bool success { false };
QDir backupDir { _backupDirectory };
auto backupFilePath { backupDir.filePath(backupName) };
QFile backupFile { backupFilePath };
if (backupFile.open(QIODevice::ReadOnly)) {
QuaZip zip { &backupFile };
success = recoverFromBackupZip(backupName, zip);
backupFile.close();
} else {
success = false;
qWarning() << "Failed to open backup file for reading: " << backupFilePath;
}
promise->resolve({
{ "success", success }
});
}
void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "recoverFromUploadedBackup", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(QByteArray, uploadedBackup));
return;
}
qDebug() << "Recovering from uploaded content archive";
// create a buffer and then a QuaZip from that buffer
QBuffer uploadedBackupBuffer { &uploadedBackup };
QuaZip uploadedZip { &uploadedBackupBuffer };
QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip";
bool success = recoverFromBackupZip(backupName, uploadedZip);
promise->resolve({
{ "success", success }
});
}
std::vector<BackupItemInfo> DomainContentBackupManager::getAllBackups() {
QDir backupDir { _backupDirectory };
auto matchingFiles =
backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + "*.zip", MANUAL_BACKUP_PREFIX + "*.zip" },
QDir::Files | QDir::NoSymLinks, QDir::Name);
QString prefixFormat = "(" + QRegExp::escape(AUTOMATIC_BACKUP_PREFIX) + "|" + QRegExp::escape(MANUAL_BACKUP_PREFIX) + ")";
QString nameFormat = "(.+)";
QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")";
QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" };
std::vector<BackupItemInfo> backups;
for (const auto& fileInfo : matchingFiles) {
auto fileName = fileInfo.fileName();
if (backupNameFormat.exactMatch(fileName)) {
auto type = backupNameFormat.cap(1);
auto name = backupNameFormat.cap(2);
auto dateTime = backupNameFormat.cap(3);
auto createdAt = QDateTime::fromString(dateTime, DATETIME_FORMAT);
if (!createdAt.isValid()) {
qDebug().nospace() << "Skipping backup (" << fileName << ") with invalid timestamp: " << dateTime;
continue;
}
backups.emplace_back(fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt,
type == MANUAL_BACKUP_PREFIX);
}
}
return backups;
}
void DomainContentBackupManager::getAllBackupsAndStatus(MiniPromise::Promise promise) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "getAllBackupsAndStatus", Q_ARG(MiniPromise::Promise, promise));
return;
}
auto backups = getAllBackups();
QVariantList variantBackups;
for (auto& backup : backups) {
bool isAvailable { true };
bool isCorrupted { false };
float availabilityProgress { 0.0f };
for (auto& handler : _backupHandlers) {
bool handlerIsAvailable { true };
float progress { 0.0f };
std::tie(handlerIsAvailable, progress) = handler->isAvailable(backup.id);
isAvailable &= handlerIsAvailable;
availabilityProgress += progress / _backupHandlers.size();
isCorrupted = isCorrupted || handler->isCorruptedBackup(backup.id);
}
variantBackups.push_back(QVariantMap({
{ "id", backup.id },
{ "name", backup.name },
{ "createdAtMillis", backup.createdAt.toMSecsSinceEpoch() },
{ "isAvailable", isAvailable },
{ "availabilityProgress", availabilityProgress },
{ "isManualBackup", backup.isManualBackup },
{ "isCorrupted", isCorrupted }
}));
}
float recoveryProgress = 0.0f;
bool isRecovering = _isRecovering.load();
if (_isRecovering) {
for (auto& handler : _backupHandlers) {
float progress = handler->getRecoveryStatus().second;
recoveryProgress += progress / _backupHandlers.size();
}
}
QVariantMap status {
{ "isRecovering", isRecovering },
{ "recoveringBackupId", _recoveryFilename },
{ "recoveryProgress", recoveryProgress }
};
QVariantMap info {
{ "backups", variantBackups },
{ "status", status }
};
promise->resolve(info);
}
void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) {
QDir backupDir { _backupDirectory };
if (backupDir.exists() && rule.maxBackupVersions > 0) {
qCDebug(domain_server) << "Rolling old backup versions for rule" << rule.name;
auto matchingFiles =
backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name);
int backupsToDelete = matchingFiles.length() - rule.maxBackupVersions;
if (backupsToDelete <= 0) {
qCDebug(domain_server) << "Found" << matchingFiles.length() << "backups, no backups need to be deleted";
} else {
qCDebug(domain_server) << "Found" << matchingFiles.length() << "backups, deleting " << backupsToDelete << "backup(s)";
for (int i = 0; i < backupsToDelete; ++i) {
auto fileInfo = matchingFiles[i].absoluteFilePath();
QFile backupFile(fileInfo);
if (backupFile.remove()) {
qCDebug(domain_server) << "Removed old backup: " << backupFile.fileName();
} else {
qCDebug(domain_server) << "Failed to remove old backup: " << backupFile.fileName();
}
}
qCDebug(domain_server) << "Done removing old backup versions";
}
} else {
qCDebug(domain_server) << "Rolling backups for rule" << rule.name << "."
<< " Max Rolled Backup Versions less than 1 [" << rule.maxBackupVersions << "]."
<< " No need to roll backups";
}
}
void DomainContentBackupManager::backup() {
auto nowDateTime = QDateTime::currentDateTime();
auto nowSeconds = nowDateTime.toSecsSinceEpoch();
for (BackupRule& rule : _backupRules) {
auto secondsSinceLastBackup = nowSeconds - rule.lastBackupSeconds;
qCDebug(domain_server) << "Checking [" << rule.name << "] - Time since last backup [" << secondsSinceLastBackup
<< "] "
<< "compared to backup interval [" << rule.intervalSeconds << "]...";
if (secondsSinceLastBackup > rule.intervalSeconds) {
qCDebug(domain_server) << "Time since last backup [" << secondsSinceLastBackup << "] for rule [" << rule.name
<< "] exceeds backup interval [" << rule.intervalSeconds << "] doing backup now...";
bool success;
QString path;
std::tie(success, path) = createBackup(AUTOMATIC_BACKUP_PREFIX, rule.extensionFormat);
if (!success) {
qCWarning(domain_server) << "Failed to create backup for" << rule.name << "at" << path;
continue;
}
qDebug() << "Created backup: " << path;
rule.lastBackupSeconds = nowSeconds;
removeOldBackupVersions(rule);
} else {
qCDebug(domain_server) << "Backup not needed for this rule [" << rule.name << "]...";
}
}
}
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;
}
QDir backupDir { _backupDirectory };
if (!backupDir.exists()) {
qCritical() << "Backup directory does not exist, bailing consolidation of backup";
promise->resolve({ { "success", false } });
return;
}
auto filePath = backupDir.absoluteFilePath(fileName);
auto copyFilePath = QDir::tempPath() + "/" + fileName;
{
QFile copyFile(copyFilePath);
copyFile.remove();
copyFile.close();
}
auto copySuccess = QFile::copy(filePath, copyFilePath);
if (!copySuccess) {
qCritical() << "Failed to create copy of backup.";
promise->resolve({ { "success", false } });
return;
}
QuaZip zip(copyFilePath);
if (!zip.open(QuaZip::mdAdd)) {
qCritical() << "Could not open backup archive:" << filePath;
qCritical() << " ERROR:" << zip.getZipError();
promise->resolve({ { "success", false } });
return;
}
for (auto& handler : _backupHandlers) {
handler->consolidateBackup(fileName, zip);
}
zip.close();
if (zip.getZipError() != UNZ_OK) {
qCritical() << "Failed to consolidate backup: " << zip.getZipError();
promise->resolve({ { "success", false } });
return;
}
promise->resolve({
{ "success", true },
{ "backupFilePath", copyFilePath }
});
}
void DomainContentBackupManager::createManualBackup(MiniPromise::Promise promise, const QString& name) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "createManualBackup", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(const QString&, name));
return;
}
QRegExp nameRE { MANUAL_BACKUP_NAME_RE };
bool success;
if (!nameRE.exactMatch(name)) {
qDebug() << "Cannot create manual backup with invalid name: " << name;
success = false;
} else {
QString path;
std::tie(success, path) = createBackup(MANUAL_BACKUP_PREFIX, name);
}
promise->resolve({
{ "success", success }
});
}
std::pair<bool, QString> DomainContentBackupManager::createBackup(const QString& prefix, const QString& name) {
auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT);
auto fileName = prefix + name + "-" + timestamp + ".zip";
auto path = _backupDirectory + "/" + fileName;
QuaZip zip(path);
if (!zip.open(QuaZip::mdAdd)) {
qCWarning(domain_server) << "Failed to open zip file at " << path;
qCWarning(domain_server) << " ERROR:" << zip.getZipError();
return { false, path };
}
for (auto& handler : _backupHandlers) {
handler->createBackup(fileName, zip);
}
zip.close();
return { true, path };
}

View file

@ -0,0 +1,106 @@
//
// DomainContentBackupManager.h
// libraries/domain-server/src
//
// Created by Ryan Huffman on 1/01/18.
// Adapted from OctreePersistThread
// 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
//
#ifndef hifi_DomainContentBackupManager_h
#define hifi_DomainContentBackupManager_h
#include <QString>
#include <QVector>
#include <QDateTime>
#include <GenericThread.h>
#include "BackupHandler.h"
#include <shared/MiniPromises.h>
#include <PortableHighResolutionClock.h>
struct BackupItemInfo {
BackupItemInfo(QString pId, QString pName, QString pAbsolutePath, QDateTime pCreatedAt, bool pIsManualBackup) :
id(pId), name(pName), absolutePath(pAbsolutePath), createdAt(pCreatedAt), isManualBackup(pIsManualBackup) { };
QString id;
QString name;
QString absolutePath;
QDateTime createdAt;
bool isManualBackup;
};
class DomainContentBackupManager : public GenericThread {
Q_OBJECT
public:
class BackupRule {
public:
QString name;
int intervalSeconds;
QString extensionFormat;
int maxBackupVersions;
qint64 lastBackupSeconds;
};
static const std::chrono::seconds DEFAULT_PERSIST_INTERVAL;
DomainContentBackupManager(const QString& rootBackupDirectory,
const QVariantList& settings,
std::chrono::milliseconds persistInterval = DEFAULT_PERSIST_INTERVAL,
bool debugTimestampNow = false);
std::vector<BackupItemInfo> getAllBackups();
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);
public slots:
void getAllBackupsAndStatus(MiniPromise::Promise promise);
void createManualBackup(MiniPromise::Promise promise, const QString& name);
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();
void recoveryCompleted();
protected:
/// Implements generic processing behavior for this thread.
virtual void setup() override;
virtual bool process() override;
virtual void shutdown() override;
void backup();
void removeOldBackupVersions(const BackupRule& rule);
void refreshBackupRules();
bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime);
int64_t getMostRecentBackupTimeInSecs(const QString& format);
void parseBackupRules(const QVariantList& backupRules);
std::pair<bool, QString> createBackup(const QString& prefix, const QString& name);
bool recoverFromBackupZip(const QString& backupName, QuaZip& backupZip);
private:
const QString _backupDirectory;
std::vector<BackupHandlerPointer> _backupHandlers;
std::chrono::milliseconds _persistInterval { 0 };
std::atomic<bool> _isRecovering { false };
QString _recoveryFilename { };
p_high_resolution_clock::time_point _lastCheck;
std::vector<BackupRule> _backupRules;
};
#endif // hifi_DomainContentBackupManager_h

View file

@ -435,10 +435,11 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
if (!userPerms.can(NodePermissions::Permission::canConnectPastMaxCapacity) && !isWithinMaxCapacity()) {
// we can't allow this user to connect because we are at max capacity
QString redirectOnMaxCapacity;
const QVariant* redirectOnMaxCapacityVariant =
valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION);
if (redirectOnMaxCapacityVariant && redirectOnMaxCapacityVariant->canConvert<QString>()) {
redirectOnMaxCapacity = redirectOnMaxCapacityVariant->toString();
QVariant redirectOnMaxCapacityVariant =
_server->_settingsManager.valueForKeyPath(MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION);
if (redirectOnMaxCapacityVariant.canConvert<QString>()) {
redirectOnMaxCapacity = redirectOnMaxCapacityVariant.toString();
qDebug() << "Redirection domain:" << redirectOnMaxCapacity;
}
@ -610,9 +611,9 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
bool DomainGatekeeper::isWithinMaxCapacity() {
// find out what our maximum capacity is
const QVariant* maximumUserCapacityVariant =
valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY);
unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0;
QVariant maximumUserCapacityVariant =
_server->_settingsManager.valueForKeyPath(MAXIMUM_USER_CAPACITY);
unsigned int maximumUserCapacity = maximumUserCapacityVariant.isValid() ? maximumUserCapacityVariant.toUInt() : 0;
if (maximumUserCapacity > 0) {
unsigned int connectedUsers = _server->countConnectedUsers();

View file

@ -84,21 +84,22 @@ void DomainMetadata::descriptorsChanged() {
// get descriptors
assert(_metadata[DESCRIPTORS].canConvert<QVariantMap>());
auto& state = *static_cast<QVariantMap*>(_metadata[DESCRIPTORS].data());
auto& settings = static_cast<DomainServer*>(parent())->_settingsManager.getSettingsMap();
auto& descriptors = static_cast<DomainServer*>(parent())->_settingsManager.getDescriptorsMap();
static const QString DESCRIPTORS_GROUP_KEYPATH = "descriptors";
auto descriptorsMap = static_cast<DomainServer*>(parent())->_settingsManager.valueForKeyPath(DESCRIPTORS).toMap();
// copy simple descriptors (description/maturity)
state[Descriptors::DESCRIPTION] = descriptors[Descriptors::DESCRIPTION];
state[Descriptors::MATURITY] = descriptors[Descriptors::MATURITY];
state[Descriptors::DESCRIPTION] = descriptorsMap[Descriptors::DESCRIPTION];
state[Descriptors::MATURITY] = descriptorsMap[Descriptors::MATURITY];
// copy array descriptors (hosts/tags)
state[Descriptors::HOSTS] = descriptors[Descriptors::HOSTS].toList();
state[Descriptors::TAGS] = descriptors[Descriptors::TAGS].toList();
state[Descriptors::HOSTS] = descriptorsMap[Descriptors::HOSTS].toList();
state[Descriptors::TAGS] = descriptorsMap[Descriptors::TAGS].toList();
// parse capacity
static const QString CAPACITY = "security.maximum_user_capacity";
const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY);
unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0;
QVariant capacityVariant = static_cast<DomainServer*>(parent())->_settingsManager.valueForKeyPath(CAPACITY);
unsigned int capacity = capacityVariant.isValid() ? capacityVariant.toUInt() : 0;
state[Descriptors::CAPACITY] = capacity;
#if DEV_BUILD || PR_BUILD

View file

@ -24,8 +24,10 @@
#include <QTimer>
#include <QUrlQuery>
#include <QCommandLineParser>
#include <QUuid>
#include <AccountManager.h>
#include <AssetClient.h>
#include <BuildInfo.h>
#include <DependencyManager.h>
#include <HifiConfigVariantMap.h>
@ -43,10 +45,20 @@
#include <Trace.h>
#include <StatTracker.h>
#include "AssetsBackupHandler.h"
#include "ContentSettingsBackupHandler.h"
#include "DomainServerNodeData.h"
#include "EntitiesBackupHandler.h"
#include "NodeConnectionData.h"
#include <Gzip.h>
#include <OctreeDataUtils.h>
Q_LOGGING_CATEGORY(domain_server, "hifi.domain_server")
const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token";
const QString DomainServer::REPLACEMENT_FILE_EXTENSION = ".replace";
int const DomainServer::EXIT_CODE_REBOOT = 234923;
@ -63,8 +75,8 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection,
std::initializer_list<QString> optionalData,
bool requireAccessToken) {
auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH);
if (accessTokenVariant == nullptr && requireAccessToken) {
auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH);
if (!accessTokenVariant.isValid() && requireAccessToken) {
connection->respond(HTTPConnection::StatusCode400, "User access token has not been set");
return true;
}
@ -100,8 +112,8 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection,
req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (accessTokenVariant != nullptr) {
auto accessTokenHeader = QString("Bearer ") + accessTokenVariant->toString();
if (accessTokenVariant.isValid()) {
auto accessTokenHeader = QString("Bearer ") + accessTokenVariant.toString();
req.setRawHeader("Authorization", accessTokenHeader.toLatin1());
}
@ -279,6 +291,27 @@ DomainServer::DomainServer(int argc, char* argv[]) :
qDebug() << "Ignoring subnet in whitelist, invalid ip portion: " << subnet;
}
}
if (QDir(getEntitiesDirPath()).mkpath(".")) {
qCDebug(domain_server) << "Created entities data directory";
}
maybeHandleReplacementEntityFile();
static const QString BACKUP_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules";
auto backupRulesVariant = _settingsManager.valueOrDefaultValueForKeyPath(BACKUP_RULES_KEYPATH);
_contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), backupRulesVariant.toList()));
connect(_contentManager.get(), &DomainContentBackupManager::started, _contentManager.get(), [this](){
_contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath())));
_contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir())));
_contentManager->addBackupHandler(BackupHandlerPointer(new ContentSettingsBackupHandler(_settingsManager)));
});
_contentManager->initialize(true);
connect(_contentManager.get(), &DomainContentBackupManager::recoveryCompleted, this, &DomainServer::restart);
}
void DomainServer::parseCommandLine() {
@ -343,6 +376,17 @@ void DomainServer::parseCommandLine() {
DomainServer::~DomainServer() {
qInfo() << "Domain Server is shutting down.";
if (_contentManager) {
_contentManager->aboutToFinish();
_contentManager->terminate();
}
// cleanup the AssetClient thread
DependencyManager::destroy<AssetClient>();
_assetClientThread.quit();
_assetClientThread.wait();
// destroy the LimitedNodeList before the DomainServer QCoreApplication is down
DependencyManager::destroy<LimitedNodeList>();
}
@ -361,7 +405,7 @@ void DomainServer::restart() {
exit(DomainServer::EXIT_CODE_REBOOT);
}
const QUuid& DomainServer::getID() {
QUuid DomainServer::getID() {
return DependencyManager::get<LimitedNodeList>()->getSessionUUID();
}
@ -370,8 +414,8 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() {
const QString X509_PRIVATE_KEY_OPTION = "key";
const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE";
QString certPath = _settingsManager.getSettingsMap().value(X509_CERTIFICATE_OPTION).toString();
QString keyPath = _settingsManager.getSettingsMap().value(X509_PRIVATE_KEY_OPTION).toString();
QString certPath = _settingsManager.valueForKeyPath(X509_CERTIFICATE_OPTION).toString();
QString keyPath = _settingsManager.valueForKeyPath(X509_PRIVATE_KEY_OPTION).toString();
if (!certPath.isEmpty() && !keyPath.isEmpty()) {
// the user wants to use the following cert and key for HTTPS
@ -414,8 +458,7 @@ bool DomainServer::optionallySetupOAuth() {
const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET";
const QString REDIRECT_HOSTNAME_OPTION = "hostname";
const QVariantMap& settingsMap = _settingsManager.getSettingsMap();
_oauthProviderURL = QUrl(settingsMap.value(OAUTH_PROVIDER_URL_OPTION).toString());
_oauthProviderURL = QUrl(_settingsManager.valueForKeyPath(OAUTH_PROVIDER_URL_OPTION).toString());
// if we don't have an oauth provider URL then we default to the default node auth url
if (_oauthProviderURL.isEmpty()) {
@ -425,9 +468,9 @@ bool DomainServer::optionallySetupOAuth() {
auto accountManager = DependencyManager::get<AccountManager>();
accountManager->setAuthURL(_oauthProviderURL);
_oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString();
_oauthClientID = _settingsManager.valueForKeyPath(OAUTH_CLIENT_ID_OPTION).toString();
_oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV);
_hostname = settingsMap.value(REDIRECT_HOSTNAME_OPTION).toString();
_hostname = _settingsManager.valueForKeyPath(REDIRECT_HOSTNAME_OPTION).toString();
if (!_oauthClientID.isEmpty()) {
if (_oauthProviderURL.isEmpty()
@ -452,11 +495,11 @@ static const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id";
void DomainServer::getTemporaryName(bool force) {
// check if we already have a domain ID
const QVariant* idValueVariant = valueForKeyPath(_settingsManager.getSettingsMap(), METAVERSE_DOMAIN_ID_KEY_PATH);
QVariant idValueVariant = _settingsManager.valueForKeyPath(METAVERSE_DOMAIN_ID_KEY_PATH);
qInfo() << "Requesting temporary domain name";
if (idValueVariant) {
qDebug() << "A domain ID is already present in domain-server settings:" << idValueVariant->toString();
if (idValueVariant.isValid()) {
qDebug() << "A domain ID is already present in domain-server settings:" << idValueVariant.toString();
if (force) {
qDebug() << "Requesting temporary domain name to replace current ID:" << getID();
} else {
@ -494,10 +537,7 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) {
// store the new domain ID and auto network setting immediately
QString newSettingsJSON = QString("{\"metaverse\": { \"id\": \"%1\", \"automatic_networking\": \"full\"}}").arg(id);
auto settingsDocument = QJsonDocument::fromJson(newSettingsJSON.toUtf8());
_settingsManager.recurseJSONObjectAndOverwriteSettings(settingsDocument.object());
// store the new ID and auto networking setting on disk
_settingsManager.persistToFile();
_settingsManager.recurseJSONObjectAndOverwriteSettings(settingsDocument.object(), DomainSettings);
// store the new token to the account info
auto accountManager = DependencyManager::get<AccountManager>();
@ -600,8 +640,6 @@ void DomainServer::setupNodeListAndAssignments() {
QVariant localPortValue = _settingsManager.valueOrDefaultValueForKeyPath(CUSTOM_LOCAL_PORT_OPTION);
int domainServerPort = localPortValue.toInt();
QVariantMap& settingsMap = _settingsManager.getSettingsMap();
int domainServerDTLSPort = INVALID_PORT;
if (_isUsingDTLS) {
@ -609,8 +647,9 @@ void DomainServer::setupNodeListAndAssignments() {
const QString CUSTOM_DTLS_PORT_OPTION = "dtls-port";
if (settingsMap.contains(CUSTOM_DTLS_PORT_OPTION)) {
domainServerDTLSPort = (unsigned short) settingsMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt();
auto dtlsPortVariant = _settingsManager.valueForKeyPath(CUSTOM_DTLS_PORT_OPTION);
if (dtlsPortVariant.isValid()) {
domainServerDTLSPort = (unsigned short) dtlsPortVariant.toUInt();
}
}
@ -640,9 +679,9 @@ void DomainServer::setupNodeListAndAssignments() {
nodeList->setSessionUUID(_overridingDomainID);
isMetaverseDomain = true; // assume metaverse domain
} else {
const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH);
if (idValueVariant) {
nodeList->setSessionUUID(idValueVariant->toString());
QVariant idValueVariant = _settingsManager.valueForKeyPath(METAVERSE_DOMAIN_ID_KEY_PATH);
if (idValueVariant.isValid()) {
nodeList->setSessionUUID(idValueVariant.toString());
isMetaverseDomain = true; // if we have an ID, we'll assume we're a metaverse domain
} else {
nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID
@ -684,11 +723,22 @@ void DomainServer::setupNodeListAndAssignments() {
packetReceiver.registerListener(PacketType::ICEServerHeartbeatDenied, this, "processICEServerHeartbeatDenialPacket");
packetReceiver.registerListener(PacketType::ICEServerHeartbeatACK, this, "processICEServerHeartbeatACK");
// add whatever static assignments that have been parsed to the queue
addStaticAssignmentsToQueue();
packetReceiver.registerListener(PacketType::OctreeDataFileRequest, this, "processOctreeDataRequestMessage");
packetReceiver.registerListener(PacketType::OctreeDataPersist, this, "processOctreeDataPersistMessage");
packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacementRequest");
packetReceiver.registerListener(PacketType::OctreeFileReplacementFromUrl, this, "handleOctreeFileReplacementFromURLRequest");
// set a custom packetVersionMatch as the verify packet operator for the udt::Socket
nodeList->setPacketFilterOperator(&DomainServer::isPacketVerified);
_assetClientThread.setObjectName("AssetClient Thread");
auto assetClient = DependencyManager::set<AssetClient>();
assetClient->moveToThread(&_assetClientThread);
_assetClientThread.start();
// add whatever static assignments that have been parsed to the queue
addStaticAssignmentsToQueue();
}
bool DomainServer::resetAccountManagerAccessToken() {
@ -699,10 +749,10 @@ bool DomainServer::resetAccountManagerAccessToken() {
QString accessToken = QProcessEnvironment::systemEnvironment().value(ENV_ACCESS_TOKEN_KEY);
if (accessToken.isEmpty()) {
const QVariant* accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH);
QVariant accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH);
if (accessTokenVariant && accessTokenVariant->canConvert(QMetaType::QString)) {
accessToken = accessTokenVariant->toString();
if (accessTokenVariant.canConvert(QMetaType::QString)) {
accessToken = accessTokenVariant.toString();
} else {
qWarning() << "No access token is present. Some operations that use the metaverse API will fail.";
qDebug() << "Set an access token via the web interface, in your user config"
@ -833,31 +883,26 @@ void DomainServer::updateICEServerAddresses() {
}
void DomainServer::parseAssignmentConfigs(QSet<Assignment::Type>& excludedTypes) {
const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)";
QRegExp assignmentConfigRegex(ASSIGNMENT_CONFIG_REGEX_STRING);
const QVariantMap& settingsMap = _settingsManager.getSettingsMap();
const QString ASSIGNMENT_CONFIG_PREFIX = "config-";
// scan for assignment config keys
QStringList variantMapKeys = settingsMap.keys();
int configIndex = variantMapKeys.indexOf(assignmentConfigRegex);
for (int i = 0; i < Assignment::AllTypes; ++i) {
QVariant assignmentConfigVariant = _settingsManager.valueOrDefaultValueForKeyPath(ASSIGNMENT_CONFIG_PREFIX + QString::number(i));
while (configIndex != -1) {
// figure out which assignment type this matches
Assignment::Type assignmentType = (Assignment::Type) assignmentConfigRegex.cap(1).toInt();
if (assignmentConfigVariant.isValid()) {
// figure out which assignment type this matches
Assignment::Type assignmentType = static_cast<Assignment::Type>(i);
if (assignmentType < Assignment::AllTypes && !excludedTypes.contains(assignmentType)) {
QVariant mapValue = settingsMap[variantMapKeys[configIndex]];
QVariantList assignmentList = mapValue.toList();
if (!excludedTypes.contains(assignmentType)) {
QVariantList assignmentList = assignmentConfigVariant.toList();
if (assignmentType != Assignment::AgentType) {
createStaticAssignmentsForType(assignmentType, assignmentList);
if (assignmentType != Assignment::AgentType) {
createStaticAssignmentsForType(assignmentType, assignmentList);
}
excludedTypes.insert(assignmentType);
}
excludedTypes.insert(assignmentType);
}
configIndex = variantMapKeys.indexOf(assignmentConfigRegex, configIndex + 1);
}
}
@ -869,10 +914,10 @@ void DomainServer::addStaticAssignmentToAssignmentHash(Assignment* newAssignment
void DomainServer::populateStaticScriptedAssignmentsFromSettings() {
const QString PERSISTENT_SCRIPTS_KEY_PATH = "scripts.persistent_scripts";
const QVariant* persistentScriptsVariant = valueForKeyPath(_settingsManager.getSettingsMap(), PERSISTENT_SCRIPTS_KEY_PATH);
QVariant persistentScriptsVariant = _settingsManager.valueOrDefaultValueForKeyPath(PERSISTENT_SCRIPTS_KEY_PATH);
if (persistentScriptsVariant) {
QVariantList persistentScriptsList = persistentScriptsVariant->toList();
if (persistentScriptsVariant.isValid()) {
QVariantList persistentScriptsList = persistentScriptsVariant.toList();
foreach(const QVariant& persistentScriptVariant, persistentScriptsList) {
QVariantMap persistentScript = persistentScriptVariant.toMap();
@ -1682,10 +1727,96 @@ void DomainServer::sendHeartbeatToIceServer() {
} else {
qDebug() << "Not sending ice-server heartbeat since there is no selected ice-server.";
qDebug() << "Waiting for" << _iceServerAddr << "host lookup response";
}
}
void DomainServer::processOctreeDataPersistMessage(QSharedPointer<ReceivedMessage> message) {
qDebug() << "Received octree data persist message";
auto data = message->readAll();
auto filePath = getEntitiesFilePath();
QDir dir(getEntitiesDirPath());
if (!dir.exists()) {
qCDebug(domain_server) << "Creating entities content directory:" << dir.absolutePath();
dir.mkpath(".");
}
QFile f(filePath);
if (f.open(QIODevice::WriteOnly)) {
f.write(data);
OctreeUtils::RawEntityData entityData;
if (entityData.readOctreeDataInfoFromData(data)) {
qCDebug(domain_server) << "Wrote new entities file" << entityData.id << entityData.version;
} else {
qCDebug(domain_server) << "Failed to read new octree data info";
}
} else {
qCDebug(domain_server) << "Failed to write new entities file:" << filePath;
}
}
QString DomainServer::getContentBackupDir() {
return PathUtils::getAppDataFilePath("backups");
}
QString DomainServer::getEntitiesDirPath() {
return PathUtils::getAppDataFilePath("entities");
}
QString DomainServer::getEntitiesFilePath() {
return PathUtils::getAppDataFilePath("entities/models.json.gz");
}
QString DomainServer::getEntitiesReplacementFilePath() {
return getEntitiesFilePath().append(REPLACEMENT_FILE_EXTENSION);
}
void DomainServer::processOctreeDataRequestMessage(QSharedPointer<ReceivedMessage> message) {
qDebug() << "Got request for octree data from " << message->getSenderSockAddr();
maybeHandleReplacementEntityFile();
bool remoteHasExistingData { false };
QUuid id;
int version;
message->readPrimitive(&remoteHasExistingData);
if (remoteHasExistingData) {
constexpr size_t UUID_SIZE_BYTES = 16;
auto idData = message->read(UUID_SIZE_BYTES);
id = QUuid::fromRfc4122(idData);
message->readPrimitive(&version);
qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << version << ")";
} else {
qCDebug(domain_server) << "Entity server does not have existing data";
}
auto entityFilePath = getEntitiesFilePath();
auto reply = NLPacketList::create(PacketType::OctreeDataFileReply, QByteArray(), true, true);
OctreeUtils::RawEntityData data;
if (data.readOctreeDataInfoFromFile(entityFilePath)) {
if (data.id == id && data.version <= version) {
qCDebug(domain_server) << "ES has sufficient octree data, not sending data";
reply->writePrimitive(false);
} else {
qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.version << ")";
QFile file(entityFilePath);
if (file.open(QIODevice::ReadOnly)) {
reply->writePrimitive(true);
reply->write(file.readAll());
} else {
qCDebug(domain_server) << "Unable to load entity file";
reply->writePrimitive(false);
}
}
} else {
qCDebug(domain_server) << "Domain server does not have valid octree data";
reply->writePrimitive(false);
}
auto nodeList = DependencyManager::get<LimitedNodeList>();
nodeList->sendPacketList(std::move(reply), message->getSenderSockAddr());
}
void DomainServer::processNodeJSONStatsPacket(QSharedPointer<ReceivedMessage> packetList, SharedNodePointer sendingNode) {
auto nodeData = static_cast<DomainServerNodeData*>(sendingNode->getLinkedData());
if (nodeData) {
@ -1795,23 +1926,25 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString URI_ASSIGNMENT = "/assignment";
const QString URI_NODES = "/nodes";
const QString URI_SETTINGS = "/settings";
const QString URI_ENTITY_FILE_UPLOAD = "/content/upload";
const QString URI_CONTENT_UPLOAD = "/content/upload";
const QString URI_RESTART = "/restart";
const QString URI_API_PLACES = "/api/places";
const QString URI_API_DOMAINS = "/api/domains";
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_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}";
auto nodeList = DependencyManager::get<LimitedNodeList>();
auto getSetting = [this](QString keyPath, QVariant& value) -> bool {
QVariantMap& settingsMap = _settingsManager.getSettingsMap();
QVariant* var = valueForKeyPath(settingsMap, keyPath);
if (var == nullptr) {
auto getSetting = [this](QString keyPath, QVariant value) -> bool {
value = _settingsManager.valueForKeyPath(keyPath);
if (!value.isValid()) {
return false;
}
value = *var;
return true;
};
@ -1879,8 +2012,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
if (connection->requestOperation() == QNetworkAccessManager::GetOperation) {
const QString URI_WIZARD = "/wizard/";
const QString WIZARD_COMPLETED_ONCE_KEY_PATH = "wizard.completed_once";
const QVariant* wizardCompletedOnce = valueForKeyPath(_settingsManager.getSettingsMap(), WIZARD_COMPLETED_ONCE_KEY_PATH);
const bool completedOnce = wizardCompletedOnce && wizardCompletedOnce->toBool();
QVariant wizardCompletedOnce = _settingsManager.valueForKeyPath(WIZARD_COMPLETED_ONCE_KEY_PATH);
const bool completedOnce = wizardCompletedOnce.isValid() && wizardCompletedOnce.toBool();
if (url.path() != URI_WIZARD && url.path().endsWith('/') && !completedOnce) {
// First visit, redirect to the wizard
@ -1984,6 +2117,37 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
// send the response
connection->respond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE));
return true;
} else if (url.path() == URI_API_BACKUPS) {
auto deferred = makePromise("getAllBackupsAndStatus");
deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) {
QJsonDocument docJSON(QJsonObject::fromVariantMap(result));
connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8());
});
_contentManager->getAllBackupsAndStatus(deferred);
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([connection, JSON_MIME_TYPE](QString error, QVariantMap result) {
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)) {
connection->respond(HTTPConnection::StatusCode200, std::move(file));
} else {
qCritical(domain_server) << "Unable to load consolidated backup at:" << path << result;
connection->respond(HTTPConnection::StatusCode500, "Error opening backup");
}
} else {
connection->respond(HTTPConnection::StatusCode400);
}
});
_contentManager->consolidateBackup(deferred, id);
return true;
} else if (url.path() == URI_RESTART) {
connection->respond(HTTPConnection::StatusCode200);
@ -2071,17 +2235,52 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
connection->respond(HTTPConnection::StatusCode200);
return true;
} else if (url.path() == URI_ENTITY_FILE_UPLOAD) {
} else if (url.path() == URI_CONTENT_UPLOAD) {
// this is an entity file upload, ask the HTTPConnection to parse the data
QList<FormData> formData = connection->parseFormData();
if (formData.size() > 0 && formData[0].second.size() > 0) {
// invoke our method to hand the new octree file off to the octree server
QMetaObject::invokeMethod(this, "handleOctreeFileReplacement",
Qt::QueuedConnection, Q_ARG(QByteArray, formData[0].second));
auto& firstFormData = formData[0];
// check the file extension to see what kind of file this is
// to make sure we handle this filetype for a content restore
auto dispositionValue = QString(firstFormData.first.value("Content-Disposition"));
auto formDataFilenameRegex = QRegExp("filename=\"(.+)\"");
auto matchIndex = formDataFilenameRegex.indexIn(dispositionValue);
QString uploadedFilename = "";
if (matchIndex != -1) {
uploadedFilename = formDataFilenameRegex.cap(1);
}
if (uploadedFilename.endsWith(".json", Qt::CaseInsensitive)
|| uploadedFilename.endsWith(".json.gz", Qt::CaseInsensitive)) {
// invoke our method to hand the new octree file off to the octree server
QMetaObject::invokeMethod(this, "handleOctreeFileReplacement",
Qt::QueuedConnection, Q_ARG(QByteArray, firstFormData.second));
// respond with a 200 for success
connection->respond(HTTPConnection::StatusCode200);
} else if (uploadedFilename.endsWith(".zip", Qt::CaseInsensitive)) {
auto deferred = makePromise("recoverFromUploadedBackup");
deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) {
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
_contentManager->recoverFromUploadedBackup(deferred, firstFormData.second);
return true;
} else {
// we don't have handling for this filetype, send back a 400 for failure
connection->respond(HTTPConnection::StatusCode400);
}
// respond with a 200 for success
connection->respond(HTTPConnection::StatusCode200);
} else {
// respond with a 400 for failure
connection->respond(HTTPConnection::StatusCode400);
@ -2089,16 +2288,50 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
return true;
} else if (url.path() == URI_API_BACKUPS) {
auto params = connection->parseUrlEncodedForm();
auto it = params.find("name");
if (it == params.end()) {
connection->respond(HTTPConnection::StatusCode400, "Bad request, missing `name`");
return true;
}
auto deferred = makePromise("createManualBackup");
deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) {
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
_contentManager->createManualBackup(deferred, it.value());
return true;
} else if (url.path() == "/domain_settings") {
auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH);
if (!accessTokenVariant) {
auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH);
if (!accessTokenVariant.isValid()) {
connection->respond(HTTPConnection::StatusCode400);
return true;
}
} else if (url.path() == URI_API_DOMAINS) {
return forwardMetaverseAPIRequest(connection, "/api/v1/domains", "domain", { "label" });
} else if (url.path().startsWith(URI_API_BACKUPS_RECOVER)) {
auto id = url.path().mid(QString(URI_API_BACKUPS_RECOVER).length());
auto deferred = makePromise("recoverFromBackup");
deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) {
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
_contentManager->recoverFromBackup(deferred, id);
return true;
}
} else if (connection->requestOperation() == QNetworkAccessManager::PutOperation) {
if (url.path() == URI_API_DOMAINS) {
@ -2111,8 +2344,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
return forwardMetaverseAPIRequest(connection, "/api/v1/domains/" + domainID, "domain",
{ }, { "network_address", "network_port", "label" });
} else if (url.path() == URI_API_PLACES) {
auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH);
if (!accessTokenVariant->isValid()) {
auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH);
if (!accessTokenVariant.isValid()) {
connection->respond(HTTPConnection::StatusCode400, "User access token has not been set");
return true;
}
@ -2160,7 +2393,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QUrl url { NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/api/v1/places/" + place_id };
url.setQuery("access_token=" + accessTokenVariant->toString());
url.setQuery("access_token=" + accessTokenVariant.toString());
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
@ -2187,7 +2420,22 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QRegExp allNodesDeleteRegex(ALL_NODE_DELETE_REGEX_STRING);
QRegExp nodeDeleteRegex(NODE_DELETE_REGEX_STRING);
if (nodeDeleteRegex.indexIn(url.path()) != -1) {
if (url.path().startsWith(URI_API_BACKUPS_ID)) {
auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length());
auto deferred = makePromise("deleteBackup");
deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) {
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
_contentManager->deleteBackup(deferred, id);
return true;
} else if (nodeDeleteRegex.indexIn(url.path()) != -1) {
// this is a request to DELETE one node by UUID
// pull the captured string, if it exists
@ -2340,10 +2588,11 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server.";
QVariantMap& settingsMap = _settingsManager.getSettingsMap();
QVariant adminUsersVariant = _settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY);
QVariant adminRolesVariant = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY);
if (!_oauthProviderURL.isEmpty()
&& (settingsMap.contains(ADMIN_USERS_CONFIG_KEY) || settingsMap.contains(ADMIN_ROLES_CONFIG_KEY))) {
&& (adminUsersVariant.isValid() || adminRolesVariant.isValid())) {
QString cookieString = connection->requestHeaders().value(HTTP_COOKIE_HEADER_KEY);
const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)";
@ -2354,7 +2603,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
cookieUUID = cookieUUIDRegex.cap(1);
}
if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) {
if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) {
qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication."
<< "These cannot be combined - using OAuth for authentication.";
}
@ -2364,13 +2613,13 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
DomainServerWebSessionData sessionData = _cookieSessionHash.value(cookieUUID);
QString profileUsername = sessionData.getUsername();
if (settingsMap.value(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) {
if (_settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) {
// this is an authenticated user
return true;
}
// loop the roles of this user and see if they are in the admin-roles array
QStringList adminRolesArray = settingsMap.value(ADMIN_ROLES_CONFIG_KEY).toStringList();
QStringList adminRolesArray = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY).toStringList();
if (!adminRolesArray.isEmpty()) {
foreach(const QString& userRole, sessionData.getRoles()) {
@ -2415,7 +2664,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
// we don't know about this user yet, so they are not yet authenticated
return false;
}
} else if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) {
} else if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) {
// config file contains username and password combinations for basic auth
const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization";
@ -2434,10 +2683,10 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
QString headerPassword = credentialList[1];
// we've pulled a username and password - now check if there is a match in our basic auth hash
QString settingsUsername = valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)->toString();
const QVariant* settingsPasswordVariant = valueForKeyPath(settingsMap, BASIC_AUTH_PASSWORD_KEY_PATH);
QString settingsUsername = _settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).toString();
QVariant settingsPasswordVariant = _settingsManager.valueForKeyPath(BASIC_AUTH_PASSWORD_KEY_PATH);
QString settingsPassword = settingsPasswordVariant ? settingsPasswordVariant->toString() : "";
QString settingsPassword = settingsPasswordVariant.isValid() ? settingsPasswordVariant.toString() : "";
QString hexHeaderPassword = headerPassword.isEmpty() ?
"" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex();
@ -2574,13 +2823,14 @@ ReplicationServerInfo serverInformationFromSettings(QVariantMap serverMap, Repli
}
void DomainServer::updateReplicationNodes(ReplicationServerDirection direction) {
auto settings = _settingsManager.getSettingsMap();
if (settings.contains(BROADCASTING_SETTINGS_KEY)) {
auto broadcastSettingsVariant = _settingsManager.valueForKeyPath(BROADCASTING_SETTINGS_KEY);
if (broadcastSettingsVariant.isValid()) {
auto nodeList = DependencyManager::get<LimitedNodeList>();
std::vector<HifiSockAddr> replicationNodesInSettings;
auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap();
auto replicationSettings = broadcastSettingsVariant.toMap();
QString serversKey = direction == Upstream ? "upstream_servers" : "downstream_servers";
QString replicationDirection = direction == Upstream ? "upstream" : "downstream";
@ -2656,13 +2906,12 @@ void DomainServer::updateUpstreamNodes() {
void DomainServer::updateReplicatedNodes() {
// Make sure we have downstream nodes in our list
auto settings = _settingsManager.getSettingsMap();
static const QString REPLICATED_USERS_KEY = "users";
_replicatedUsernames.clear();
if (settings.contains(BROADCASTING_SETTINGS_KEY)) {
auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap();
auto replicationVariant = _settingsManager.valueForKeyPath(BROADCASTING_SETTINGS_KEY);
if (replicationVariant.isValid()) {
auto replicationSettings = replicationVariant.toMap();
if (replicationSettings.contains(REPLICATED_USERS_KEY)) {
auto usersSettings = replicationSettings.value(REPLICATED_USERS_KEY).toList();
for (auto& username : usersSettings) {
@ -2850,17 +3099,17 @@ void DomainServer::processPathQueryPacket(QSharedPointer<ReceivedMessage> messag
// check out paths in the _configMap to see if we have a match
auto keypath = QString(PATHS_SETTINGS_KEYPATH_FORMAT).arg(SETTINGS_PATHS_KEY).arg(pathQuery);
const QVariant* pathMatch = valueForKeyPath(_settingsManager.getSettingsMap(), keypath);
QVariant pathMatch = _settingsManager.valueForKeyPath(keypath);
if (pathMatch || pathQuery == INDEX_PATH) {
if (pathMatch.isValid() || pathQuery == INDEX_PATH) {
// we got a match, respond with the resulting viewpoint
auto nodeList = DependencyManager::get<LimitedNodeList>();
QString responseViewpoint;
// if we didn't match the path BUT this is for the index path then send back our default
if (pathMatch) {
responseViewpoint = pathMatch->toMap()[PATH_VIEWPOINT_KEY].toString();
if (pathMatch.isValid()) {
responseViewpoint = pathMatch.toMap()[PATH_VIEWPOINT_KEY].toString();
} else {
const QString DEFAULT_INDEX_PATH = "/0,0,0/0,0,0,1";
responseViewpoint = DEFAULT_INDEX_PATH;
@ -3092,19 +3341,101 @@ void DomainServer::setupGroupCacheRefresh() {
}
}
void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) {
// enumerate the nodes and find any octree type servers with active sockets
void DomainServer::maybeHandleReplacementEntityFile() {
const auto replacementFilePath = getEntitiesReplacementFilePath();
OctreeUtils::RawEntityData data;
if (!data.readOctreeDataInfoFromFile(replacementFilePath)) {
qCWarning(domain_server) << "Replacement file could not be read, it either doesn't exist or is invalid.";
} else {
qCDebug(domain_server) << "Replacing existing entity date with replacement file";
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) {
return node->getType() == NodeType::EntityServer && node->getActiveSocket();
}, [&octreeFile, limitedNodeList](const SharedNodePointer& octreeNode) {
// setup a packet to send to this octree server with the new octree file data
auto octreeFilePacketList = NLPacketList::create(PacketType::OctreeFileReplacement, QByteArray(), true, true);
octreeFilePacketList->write(octreeFile);
QFile replacementFile(replacementFilePath);
if (!replacementFile.remove()) {
// If we can't remove the replacement file, we are at risk of getting into a state where
// we continually replace the primary entity file with the replacement entity file.
qCWarning(domain_server) << "Unable to remove replacement file, bailing";
} else {
data.resetIdAndVersion();
auto gzippedData = data.toGzippedByteArray();
qDebug() << "Sending an octree file replacement of" << octreeFile.size() << "bytes to" << octreeNode;
limitedNodeList->sendPacketList(std::move(octreeFilePacketList), *octreeNode);
});
QFile currentFile(getEntitiesFilePath());
if (!currentFile.open(QIODevice::WriteOnly)) {
qCWarning(domain_server)
<< "Failed to update entities data file with replacement file, unable to open entities file for writing";
} else {
currentFile.write(gzippedData);
}
}
}
}
void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) {
//Assume we have compressed data
auto compressedOctree = octreeFile;
QByteArray jsonOctree;
bool wasCompressed = gunzip(compressedOctree, jsonOctree);
if (!wasCompressed) {
// the source was not compressed, assume we were sent regular JSON data
jsonOctree = compressedOctree;
}
OctreeUtils::RawEntityData data;
if (data.readOctreeDataInfoFromData(jsonOctree)) {
data.resetIdAndVersion();
gzip(data.toByteArray(), compressedOctree);
// write the compressed octree data to a special file
auto replacementFilePath = getEntitiesReplacementFilePath();
QFile replacementFile(replacementFilePath);
if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) {
// we've now written our replacement file, time to take the server down so it can
// process it when it comes back up
qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server";
QMetaObject::invokeMethod(this, "restart", Qt::QueuedConnection);
} else {
qWarning() << "Could not write replacement octree data to file - refusing to process";
}
} else {
qDebug() << "Received replacement octree file that is invalid - refusing to process";
}
}
void DomainServer::handleOctreeFileReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message) {
qInfo() << "Received request to replace content from a url";
auto node = DependencyManager::get<LimitedNodeList>()->findNodeWithAddr(message->getSenderSockAddr());
if (node) {
qDebug() << "Found node: " << node->getCanReplaceContent();
}
if (node->getCanReplaceContent()) {
// Convert message data into our URL
QString url(message->getMessage());
QUrl modelsURL = QUrl(url, QUrl::StrictMode);
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest request(modelsURL);
QNetworkReply* reply = networkAccessManager.get(request);
qDebug() << "Downloading JSON from: " << modelsURL;
connect(reply, &QNetworkReply::finished, [this, reply, modelsURL]() {
QNetworkReply::NetworkError networkError = reply->error();
if (networkError == QNetworkReply::NoError) {
handleOctreeFileReplacement(reply->readAll());
} else {
qDebug() << "Error downloading JSON from specified file: " << modelsURL;
}
});
}
}
void DomainServer::handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMessage> message) {
auto node = DependencyManager::get<NodeList>()->nodeWithUUID(message->getSourceID());
if (node->getCanReplaceContent()) {
handleOctreeFileReplacement(message->readAll());
}
}

View file

@ -18,6 +18,7 @@
#include <QtCore/QQueue>
#include <QtCore/QSharedPointer>
#include <QtCore/QStringList>
#include <QtCore/QThread>
#include <QtCore/QUrl>
#include <QAbstractNativeEventFilter>
@ -25,14 +26,20 @@
#include <HTTPSConnection.h>
#include <LimitedNodeList.h>
#include "AssetsBackupHandler.h"
#include "DomainGatekeeper.h"
#include "DomainMetadata.h"
#include "DomainServerSettingsManager.h"
#include "DomainServerWebSessionData.h"
#include "WalletTransaction.h"
#include "DomainContentBackupManager.h"
#include "PendingAssignedNodeData.h"
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(domain_server)
typedef QSharedPointer<Assignment> SharedAssignmentPointer;
typedef QMultiHash<QUuid, WalletTransaction*> TransactionHash;
@ -63,6 +70,8 @@ public:
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override;
bool handleHTTPSRequest(HTTPSConnection* connection, const QUrl& url, bool skipSubHandler = false) override;
static const QString REPLACEMENT_FILE_EXTENSION;
public slots:
/// Called by NodeList to inform us a node has been added
void nodeAdded(SharedNodePointer node);
@ -82,6 +91,13 @@ private slots:
void processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message);
void processICEServerHeartbeatACK(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacement(QByteArray octreeFile);
void processOctreeDataRequestMessage(QSharedPointer<ReceivedMessage> message);
void processOctreeDataPersistMessage(QSharedPointer<ReceivedMessage> message);
void setupPendingAssignmentCredits();
void sendPendingTransactionsToServer();
@ -89,8 +105,7 @@ private slots:
void sendHeartbeatToMetaverse() { sendHeartbeatToMetaverse(QString()); }
void sendHeartbeatToIceServer();
void handleConnectedNode(SharedNodePointer newNode);
void handleConnectedNode(SharedNodePointer newNode);
void handleTempDomainSuccess(QNetworkReply& requestReply);
void handleTempDomainError(QNetworkReply& requestReply);
@ -107,8 +122,6 @@ private slots:
void handleSuccessfulICEServerAddressUpdate(QNetworkReply& requestReply);
void handleFailedICEServerAddressUpdate(QNetworkReply& requestReply);
void handleOctreeFileReplacement(QByteArray octreeFile);
void updateReplicatedNodes();
void updateDownstreamNodes();
void updateUpstreamNodes();
@ -122,9 +135,16 @@ signals:
void userDisconnected();
private:
const QUuid& getID();
QUuid getID();
void parseCommandLine();
QString getContentBackupDir();
QString getEntitiesDirPath();
QString getEntitiesFilePath();
QString getEntitiesReplacementFilePath();
void maybeHandleReplacementEntityFile();
void setupNodeListAndAssignments();
bool optionallySetupOAuth();
bool optionallyReadX509KeyAndCertificate();
@ -250,7 +270,11 @@ private:
bool _sendICEServerAddressToMetaverseAPIInProgress { false };
bool _sendICEServerAddressToMetaverseAPIRedo { false };
std::unique_ptr<DomainContentBackupManager> _contentManager { nullptr };
QHash<QUuid, QPointer<HTTPSConnection>> _pendingOAuthConnections;
QThread _assetClientThread;
};

View file

@ -19,6 +19,7 @@
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QStandardPaths>
#include <QtCore/QThread>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
@ -32,15 +33,22 @@
#include <SettingHelpers.h>
#include <AvatarData.h> //for KillAvatarReason
#include <FingerprintUtils.h>
#include "DomainServerNodeData.h"
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json";
const QString SETTINGS_PATH = "/settings";
const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
const QString CONTENT_SETTINGS_PATH_JSON = "/content-settings.json";
const QString DESCRIPTION_SETTINGS_KEY = "settings";
const QString SETTING_DEFAULT_KEY = "default";
const QString DESCRIPTION_NAME_KEY = "name";
const QString DESCRIPTION_GROUP_LABEL_KEY = "label";
const QString DESCRIPTION_BACKUP_FLAG_KEY = "backup";
const QString SETTING_DESCRIPTION_TYPE_KEY = "type";
const QString DESCRIPTION_COLUMNS_KEY = "columns";
const QString CONTENT_SETTING_FLAG_KEY = "content_setting";
const QString SETTINGS_VIEWPOINT_KEY = "viewpoint";
@ -63,6 +71,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() {
if (descriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) {
_descriptionArray = descriptionDocument.object()[DESCRIPTION_SETTINGS_KEY].toArray();
splitSettingsDescription();
return;
}
}
@ -78,11 +88,99 @@ DomainServerSettingsManager::DomainServerSettingsManager() {
Q_ARG(int, MISSING_SETTINGS_DESC_ERROR_CODE));
}
void DomainServerSettingsManager::splitSettingsDescription() {
// construct separate description arrays for domain settings and content settings
// since they are displayed on different pages
// along the way we also construct one object that holds the groups separated by domain settings
// and content settings, so that the DS can setup dropdown menus below "Content" and "Settings"
// headers to jump directly to a settings group on the page of either
QJsonArray domainSettingsMenuGroups;
QJsonArray contentSettingsMenuGroups;
foreach(const QJsonValue& group, _descriptionArray) {
QJsonObject groupObject = group.toObject();
static const QString HIDDEN_GROUP_KEY = "hidden";
bool groupHidden = groupObject.contains(HIDDEN_GROUP_KEY) && groupObject[HIDDEN_GROUP_KEY].toBool();
QJsonArray domainSettingArray;
QJsonArray contentSettingArray;
foreach(const QJsonValue& settingDescription, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) {
QJsonObject settingDescriptionObject = settingDescription.toObject();
bool isContentSetting = settingDescriptionObject.contains(CONTENT_SETTING_FLAG_KEY)
&& settingDescriptionObject[CONTENT_SETTING_FLAG_KEY].toBool();
if (isContentSetting) {
// push the setting description to the pending content setting array
contentSettingArray.push_back(settingDescriptionObject);
} else {
// push the setting description to the pending domain setting array
domainSettingArray.push_back(settingDescriptionObject);
}
}
if (!domainSettingArray.isEmpty() || !contentSettingArray.isEmpty()) {
// we know for sure we'll have something to add to our settings menu groups
// so setup that object for the group now, as long as the group isn't hidden alltogether
QJsonObject settingsDropdownGroup;
if (!groupHidden) {
if (groupObject.contains(DESCRIPTION_NAME_KEY)) {
settingsDropdownGroup[DESCRIPTION_NAME_KEY] = groupObject[DESCRIPTION_NAME_KEY];
}
settingsDropdownGroup[DESCRIPTION_GROUP_LABEL_KEY] = groupObject[DESCRIPTION_GROUP_LABEL_KEY];
static const QString DESCRIPTION_GROUP_HTML_ID_KEY = "html_id";
if (groupObject.contains(DESCRIPTION_GROUP_HTML_ID_KEY)) {
settingsDropdownGroup[DESCRIPTION_GROUP_HTML_ID_KEY] = groupObject[DESCRIPTION_GROUP_HTML_ID_KEY];
}
}
if (!domainSettingArray.isEmpty()) {
// we have some domain settings from this group, add the group with the filtered settings
QJsonObject filteredGroupObject = groupObject;
filteredGroupObject[DESCRIPTION_SETTINGS_KEY] = domainSettingArray;
_domainSettingsDescription.push_back(filteredGroupObject);
// if the group isn't hidden, add its information to the domain settings menu groups
if (!groupHidden) {
domainSettingsMenuGroups.push_back(settingsDropdownGroup);
}
}
if (!contentSettingArray.isEmpty()) {
// we have some content settings from this group, add the group with the filtered settings
QJsonObject filteredGroupObject = groupObject;
filteredGroupObject[DESCRIPTION_SETTINGS_KEY] = contentSettingArray;
_contentSettingsDescription.push_back(filteredGroupObject);
// if the group isn't hidden, add its information to the content settings menu groups
if (!groupHidden) {
contentSettingsMenuGroups.push_back(settingsDropdownGroup);
}
}
}
}
// populate the settings menu groups with what we've collected
static const QString SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY = "domain_settings";
static const QString SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY = "content_settings";
_settingsMenuGroups[SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY] = domainSettingsMenuGroups;
_settingsMenuGroups[SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY] = contentSettingsMenuGroups;
}
void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer<ReceivedMessage> message) {
Assignment::Type type;
message->readPrimitive(&type);
QJsonObject responseObject = responseObjectForType(QString::number(type));
QJsonObject responseObject = settingsResponseObjectForType(QString::number(type));
auto json = QJsonDocument(responseObject).toJson();
auto packetList = NLPacketList::create(PacketType::DomainSettings, QByteArray(), true, true);
@ -94,6 +192,9 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer<Re
}
void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList) {
// since we're called from the DomainServerSettingsManager constructor, we don't take a write lock here
// even though we change the underlying config map
_argumentList = argumentList;
_configMap.loadConfig(_argumentList);
@ -297,6 +398,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
_standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canRezTemporaryCertifiedEntities);
packPermissions();
}
if (oldVersion < 2.0) {
const QString WIZARD_COMPLETED_ONCE = "wizard.completed_once";
@ -304,6 +406,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
*wizardCompletedOnce = QVariant(true);
}
if (oldVersion < 2.1) {
// convert old avatar scale settings into avatar height.
@ -314,17 +417,32 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
QVariant* avatarMinScale = _configMap.valueForKeyPath(AVATAR_MIN_SCALE_KEYPATH);
if (avatarMinScale) {
float scale = avatarMinScale->toFloat();
_configMap.valueForKeyPath(AVATAR_MIN_HEIGHT_KEYPATH, scale * DEFAULT_AVATAR_HEIGHT);
auto newMinScaleVariant = _configMap.valueForKeyPath(AVATAR_MIN_HEIGHT_KEYPATH, true);
*newMinScaleVariant = avatarMinScale->toFloat() * DEFAULT_AVATAR_HEIGHT;
}
QVariant* avatarMaxScale = _configMap.valueForKeyPath(AVATAR_MAX_SCALE_KEYPATH);
if (avatarMaxScale) {
float scale = avatarMaxScale->toFloat();
_configMap.valueForKeyPath(AVATAR_MAX_HEIGHT_KEYPATH, scale * DEFAULT_AVATAR_HEIGHT);
auto newMaxScaleVariant = _configMap.valueForKeyPath(AVATAR_MAX_HEIGHT_KEYPATH, true);
*newMaxScaleVariant = avatarMaxScale->toFloat() * DEFAULT_AVATAR_HEIGHT;
}
}
if (oldVersion < 2.2) {
// migrate entity server rolling backup intervals to new location for automatic content archive intervals
const QString ENTITY_SERVER_BACKUPS_KEYPATH = "entity_server_settings.backups";
const QString AUTO_CONTENT_ARCHIVES_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules";
QVariant* previousBackupsVariant = _configMap.valueForKeyPath(ENTITY_SERVER_BACKUPS_KEYPATH);
if (previousBackupsVariant) {
auto migratedBackupsVariant = _configMap.valueForKeyPath(AUTO_CONTENT_ARCHIVES_RULES_KEYPATH, true);
*migratedBackupsVariant = *previousBackupsVariant;
}
}
// write the current description version to our settings
*versionVariant = _descriptionVersion;
@ -335,17 +453,6 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
unpackPermissions();
}
QVariantMap& DomainServerSettingsManager::getDescriptorsMap() {
static const QString DESCRIPTORS{ "descriptors" };
auto& settingsMap = getSettingsMap();
if (!getSettingsMap().contains(DESCRIPTORS)) {
settingsMap.insert(DESCRIPTORS, QVariantMap());
}
return *static_cast<QVariantMap*>(getSettingsMap()[DESCRIPTORS].data());
}
void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap& permissionsRows,
QString groupName, NodePermissionsPointer perms) {
// this is called when someone has used the domain-settings webpage to add a group. They type the group's name
@ -374,6 +481,9 @@ void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap&
void DomainServerSettingsManager::packPermissionsForMap(QString mapName,
NodePermissionsMap& permissionsRows,
QString keyPath) {
// grab a write lock on the settings mutex since we're about to change the config map
QWriteLocker locker(&_settingsLock);
// find (or create) the "security" section of the settings map
QVariant* security = _configMap.valueForKeyPath("security", true);
if (!security->canConvert(QMetaType::QVariantMap)) {
@ -463,15 +573,20 @@ bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& key
mapPointer->clear();
QVariant* permissions = _configMap.valueForKeyPath(keyPath, true);
if (!permissions->canConvert(QMetaType::QVariantList)) {
QVariant permissions = valueOrDefaultValueForKeyPath(keyPath);
if (!permissions.isValid()) {
// we don't have a permissions object to unpack for this keypath, bail
return false;
}
if (!permissions.canConvert(QMetaType::QVariantList)) {
qDebug() << "Failed to extract permissions for key path" << keyPath << "from settings.";
(*permissions) = QVariantList();
}
bool needPack = false;
QList<QVariant> permissionsList = permissions->toList();
QList<QVariant> permissionsList = permissions.toList();
foreach (QVariant permsHash, permissionsList) {
NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) };
QString id = perms->getID();
@ -498,6 +613,11 @@ bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& key
void DomainServerSettingsManager::unpackPermissions() {
// transfer details from _configMap to _agentPermissions
// NOTE: Defaults for standard permissions (anonymous, friends, localhost, logged-in) used
// to be set here and then immediately persisted to the config JSON file.
// They have since been moved to describe-settings.json as the default value for AGENT_STANDARD_PERMISSIONS_KEYPATH.
// In order to change the default standard permissions you must change the default value in describe-settings.json.
bool needPack = false;
needPack |= unpackPermissionsForKeypath(AGENT_STANDARD_PERMISSIONS_KEYPATH, &_standardAgentPermissions);
@ -557,57 +677,39 @@ void DomainServerSettingsManager::unpackPermissions() {
}
});
// if any of the standard names are missing, add them
foreach(const QString& standardName, NodePermissions::standardNames) {
NodePermissionsKey standardKey { standardName, 0 };
if (!_standardAgentPermissions.contains(standardKey)) {
// we don't have permissions for one of the standard groups, so we'll add them now
NodePermissionsPointer perms { new NodePermissions(standardKey) };
if (standardKey == NodePermissions::standardNameLocalhost) {
// the localhost user is granted all permissions by default
perms->setAll(true);
} else {
// anonymous, logged in, and friend users get connect permissions by default
perms->set(NodePermissions::Permission::canConnectToDomain);
perms->set(NodePermissions::Permission::canRezTemporaryCertifiedEntities);
}
// add the permissions to the standard map
_standardAgentPermissions[standardKey] = perms;
// this will require a packing of permissions
needPack = true;
}
}
needPack |= ensurePermissionsForGroupRanks();
if (needPack) {
packPermissions();
}
#ifdef WANT_DEBUG
#ifdef WANT_DEBUG
qDebug() << "--------------- permissions ---------------------";
QList<QHash<NodePermissionsKey, NodePermissionsPointer>> permissionsSets;
permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get()
<< _groupPermissions.get() << _groupForbiddens.get()
<< _ipPermissions.get() << _macPermissions.get()
<< _machineFingerprintPermissions.get();
std::array<NodePermissionsMap*, 7> permissionsSets {{
&_standardAgentPermissions, &_agentPermissions,
&_groupPermissions, &_groupForbiddens,
&_ipPermissions, &_macPermissions,
&_machineFingerprintPermissions
}};
foreach (auto permissionSet, permissionsSets) {
QHashIterator<NodePermissionsKey, NodePermissionsPointer> i(permissionSet);
while (i.hasNext()) {
i.next();
NodePermissionsPointer perms = i.value();
auto& permissionKeyMap = permissionSet->get();
auto it = permissionKeyMap.begin();
while (it != permissionKeyMap.end()) {
NodePermissionsPointer perms = it->second;
if (perms->isGroup()) {
qDebug() << i.key() << perms->getGroupID() << perms;
qDebug() << it->first << perms->getGroupID() << perms;
} else {
qDebug() << i.key() << perms;
qDebug() << it->first << perms;
}
++it;
}
}
#endif
#endif
}
bool DomainServerSettingsManager::ensurePermissionsForGroupRanks() {
@ -955,12 +1057,22 @@ NodePermissions DomainServerSettingsManager::getForbiddensForGroup(const QUuid&
return getForbiddensForGroup(groupKey.first, groupKey.second);
}
QVariant DomainServerSettingsManager::valueForKeyPath(const QString& keyPath) {
QReadLocker locker(&_settingsLock);
auto foundValue = _configMap.valueForKeyPath(keyPath);
return foundValue ? *foundValue : QVariant();
}
QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) {
QReadLocker locker(&_settingsLock);
const QVariant* foundValue = _configMap.valueForKeyPath(keyPath);
if (foundValue) {
return *foundValue;
} else {
// we don't need the settings lock anymore since we're done reading from the config map
locker.unlock();
int dotIndex = keyPath.indexOf('.');
QString groupKey = keyPath.mid(0, dotIndex);
@ -986,58 +1098,279 @@ QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QStrin
}
bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection *connection, const QUrl &url) {
if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == SETTINGS_PATH_JSON) {
// this is a POST operation to change one or more settings
QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent());
QJsonObject postedObject = postedDocument.object();
if (connection->requestOperation() == QNetworkAccessManager::PostOperation) {
static const QString SETTINGS_RESTORE_PATH = "/settings/restore";
// we recurse one level deep below each group for the appropriate setting
bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject);
if (url.path() == SETTINGS_PATH_JSON || url.path() == CONTENT_SETTINGS_PATH_JSON) {
// this is a POST operation to change one or more settings
QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent());
QJsonObject postedObject = postedDocument.object();
// store whatever the current _settingsMap is to file
persistToFile();
SettingsType endpointType = url.path() == SETTINGS_PATH_JSON ? DomainSettings : ContentSettings;
// return success to the caller
QString jsonSuccess = "{\"status\": \"success\"}";
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
// we recurse one level deep below each group for the appropriate setting
bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject, endpointType);
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
if (restartRequired) {
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
} else {
unpackPermissions();
apiRefreshGroupInformation();
emit updateNodePermissions();
emit settingsUpdated();
// return success to the caller
QString jsonSuccess = "{\"status\": \"success\"}";
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
if (restartRequired) {
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
} else {
unpackPermissions();
apiRefreshGroupInformation();
emit updateNodePermissions();
emit settingsUpdated();
}
return true;
} else if (url.path() == SETTINGS_RESTORE_PATH) {
// this is an JSON settings file restore, ask the HTTPConnection to parse the data
QList<FormData> formData = connection->parseFormData();
bool wasRestoreSuccessful = false;
if (formData.size() > 0 && formData[0].second.size() > 0) {
// take the posted file and convert it to a QJsonObject
auto postedDocument = QJsonDocument::fromJson(formData[0].second);
if (postedDocument.isObject()) {
wasRestoreSuccessful = restoreSettingsFromObject(postedDocument.object(), DomainSettings);
}
}
if (wasRestoreSuccessful) {
// respond with a 200 for success
QString jsonSuccess = "{\"status\": \"success\"}";
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
} else {
// respond with a 400 for failure
connection->respond(HTTPConnection::StatusCode400);
}
return true;
}
} else if (connection->requestOperation() == QNetworkAccessManager::GetOperation) {
static const QString SETTINGS_MENU_GROUPS_PATH = "/settings-menu-groups.json";
static const QString SETTINGS_BACKUP_PATH = "/settings/backup.json";
return true;
} else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) {
// setup a JSON Object with descriptions and non-omitted settings
const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions";
const QString SETTINGS_RESPONSE_VALUE_KEY = "values";
if (url.path() == SETTINGS_PATH_JSON || url.path() == CONTENT_SETTINGS_PATH_JSON) {
QJsonObject rootObject;
rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = _descriptionArray;
rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true);
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json");
// setup a JSON Object with descriptions and non-omitted settings
const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions";
const QString SETTINGS_RESPONSE_VALUE_KEY = "values";
QJsonObject rootObject;
DomainSettingsInclusion domainSettingsInclusion = (url.path() == SETTINGS_PATH_JSON)
? IncludeDomainSettings : NoDomainSettings;
ContentSettingsInclusion contentSettingsInclusion = (url.path() == CONTENT_SETTINGS_PATH_JSON)
? IncludeContentSettings : NoContentSettings;
rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = (url.path() == SETTINGS_PATH_JSON)
? _domainSettingsDescription : _contentSettingsDescription;
// grab a domain settings object for all types, filtered for the right class of settings
// and exclude default values
rootObject[SETTINGS_RESPONSE_VALUE_KEY] = settingsResponseObjectForType("", Authenticated,
domainSettingsInclusion,
contentSettingsInclusion,
IncludeDefaultSettings);
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json");
return true;
} else if (url.path() == SETTINGS_MENU_GROUPS_PATH) {
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(_settingsMenuGroups).toJson(), "application/json");
return true;
} else if (url.path() == SETTINGS_BACKUP_PATH) {
// grab the settings backup as an authenticated user
// for the domain settings type only, excluding hidden and default values
auto currentDomainSettingsJSON = settingsResponseObjectForType("", Authenticated, IncludeDomainSettings,
NoContentSettings, NoDefaultSettings, ForBackup);
// setup headers that tell the client to download the file wth a special name
Headers downloadHeaders;
downloadHeaders.insert("Content-Transfer-Encoding", "binary");
// create a timestamped filename for the backup
const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" };
auto backupFilename = "domain-settings_" + QDateTime::currentDateTime().toString(DATETIME_FORMAT) + ".json";
downloadHeaders.insert("Content-Disposition",
QString("attachment; filename=\"%1\"").arg(backupFilename).toLocal8Bit());
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(currentDomainSettingsJSON).toJson(),
"application/force-download", downloadHeaders);
}
}
return false;
}
QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& typeValue, bool isAuthenticated) {
bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) {
// grab a write lock since we're about to change the settings map
QWriteLocker locker(&_settingsLock);
QJsonArray* filteredDescriptionArray = settingsType == DomainSettings
? &_domainSettingsDescription : &_contentSettingsDescription;
// grab a copy of the current config before restore, so that we can back out if something bad happens during
QVariantMap preRestoreConfig = _configMap.getConfig();
bool shouldCancelRestore = false;
// enumerate through the settings in the description
// if we have one in the restore then use it, otherwise clear it from current settings
foreach(const QJsonValue& descriptionGroupValue, *filteredDescriptionArray) {
QJsonObject descriptionGroupObject = descriptionGroupValue.toObject();
QString groupKey = descriptionGroupObject[DESCRIPTION_NAME_KEY].toString();
QJsonArray descriptionGroupSettings = descriptionGroupObject[DESCRIPTION_SETTINGS_KEY].toArray();
// grab the matching group from the restore so we can look at its settings
QJsonObject restoreGroup;
QVariantMap* configGroupMap = nullptr;
if (groupKey.isEmpty()) {
// this is for a setting at the root, use the full object as our restore group
restoreGroup = settingsToRestore;
// the variant map for this "group" is just the config map since there's no group
configGroupMap = &_configMap.getConfig();
} else {
if (settingsToRestore.contains(groupKey)) {
restoreGroup = settingsToRestore[groupKey].toObject();
}
// grab the variant for the group
auto groupMapVariant = _configMap.valueForKeyPath(groupKey);
// if it existed, double check that it is a map - any other value is unexpected and should cancel a restore
if (groupMapVariant) {
if (groupMapVariant->canConvert<QVariantMap>()) {
configGroupMap = static_cast<QVariantMap*>(groupMapVariant->data());
} else {
shouldCancelRestore = true;
break;
}
}
}
foreach(const QJsonValue& descriptionSettingValue, descriptionGroupSettings) {
QJsonObject descriptionSettingObject = descriptionSettingValue.toObject();
// we'll override this setting with the default or what is in the restore as long as
// it isn't specifically excluded from backups
bool isBackedUpSetting = !descriptionSettingObject.contains(DESCRIPTION_BACKUP_FLAG_KEY)
|| descriptionSettingObject[DESCRIPTION_BACKUP_FLAG_KEY].toBool();
if (isBackedUpSetting) {
QString settingName = descriptionSettingObject[DESCRIPTION_NAME_KEY].toString();
// check if we have a matching setting for this in the restore
QJsonValue restoreValue;
if (restoreGroup.contains(settingName)) {
restoreValue = restoreGroup[settingName];
}
// we should create the value for this key path in our current config map
// if we had value in the restore file
bool shouldCreateIfMissing = !restoreValue.isNull();
// get a QVariant pointer to this setting in our config map
QString fullSettingKey = !groupKey.isEmpty()
? groupKey + "." + settingName : settingName;
QVariant* variantValue = _configMap.valueForKeyPath(fullSettingKey, shouldCreateIfMissing);
if (restoreValue.isNull()) {
if (variantValue && !variantValue->isNull() && configGroupMap) {
// we didn't have a value to restore, but there might be a value in the config map
// so we need to remove the value in the config map which will set it back to the default
qDebug() << "Removing" << fullSettingKey << "from settings since it is not in the restored JSON";
configGroupMap->remove(settingName);
}
} else {
// we have a value to restore, use update setting to set it
// but clear the existing value first so that no merging between the restored settings
// and existing settings occurs
variantValue->clear();
// we might need to re-grab config group map in case it didn't exist when we looked for it before
// but was created by the call to valueForKeyPath before
if (!configGroupMap) {
auto groupMapVariant = _configMap.valueForKeyPath(groupKey);
if (groupMapVariant && groupMapVariant->canConvert<QVariantMap>()) {
configGroupMap = static_cast<QVariantMap*>(groupMapVariant->data());
} else {
shouldCancelRestore = true;
break;
}
}
qDebug() << "Updating setting" << fullSettingKey << "from restored JSON";
updateSetting(settingName, restoreValue, *configGroupMap, descriptionSettingObject);
}
}
}
if (shouldCancelRestore) {
break;
}
}
if (shouldCancelRestore) {
// if we cancelled the restore, go back to our state before and return false
qDebug() << "Restore cancelled, settings have not been changed";
_configMap.getConfig() = preRestoreConfig;
return false;
} else {
// restore completed, persist the new settings
qDebug() << "Restore completed, persisting restored settings to file";
// let go of the write lock since we're done making changes to the config map
locker.unlock();
persistToFile();
return true;
}
}
QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QString& typeValue,
SettingsRequestAuthentication authentication,
DomainSettingsInclusion domainSettingsInclusion,
ContentSettingsInclusion contentSettingsInclusion,
DefaultSettingsInclusion defaultSettingsInclusion,
SettingsBackupFlag settingsBackupFlag) {
QJsonObject responseObject;
if (!typeValue.isEmpty() || isAuthenticated) {
if (!typeValue.isEmpty() || authentication == Authenticated) {
// convert the string type value to a QJsonValue
QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt());
const QString AFFECTED_TYPES_JSON_KEY = "assignment-types";
// enumerate the groups in the description object to find which settings to pass
foreach(const QJsonValue& groupValue, _descriptionArray) {
// only enumerate the requested settings type (domain setting or content setting)
QJsonArray* filteredDescriptionArray = &_descriptionArray;
if (domainSettingsInclusion == IncludeDomainSettings && contentSettingsInclusion != IncludeContentSettings) {
filteredDescriptionArray = &_domainSettingsDescription;
} else if (contentSettingsInclusion == IncludeContentSettings && domainSettingsInclusion != IncludeDomainSettings) {
filteredDescriptionArray = &_contentSettingsDescription;
}
// enumerate the groups in the potentially filtered object to find which settings to pass
foreach(const QJsonValue& groupValue, *filteredDescriptionArray) {
QJsonObject groupObject = groupValue.toObject();
QString groupKey = groupObject[DESCRIPTION_NAME_KEY].toString();
QJsonArray groupSettingsArray = groupObject[DESCRIPTION_SETTINGS_KEY].toArray();
@ -1045,57 +1378,64 @@ QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& ty
QJsonObject groupResponseObject;
foreach(const QJsonValue& settingValue, groupSettingsArray) {
const QString VALUE_HIDDEN_FLAG_KEY = "value-hidden";
QJsonObject settingObject = settingValue.toObject();
if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool()) {
// consider this setting as long as it isn't hidden
// and either this isn't for a backup or it's a value included in backups
bool includedInBackups = !settingObject.contains(DESCRIPTION_BACKUP_FLAG_KEY)
|| settingObject[DESCRIPTION_BACKUP_FLAG_KEY].toBool();
if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool() && (settingsBackupFlag != ForBackup || includedInBackups)) {
QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray();
if (affectedTypesArray.isEmpty()) {
affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray();
}
if (affectedTypesArray.contains(queryType) ||
(queryType.isNull() && isAuthenticated)) {
// this is a setting we should include in the responseObject
(queryType.isNull() && authentication == Authenticated)) {
QString settingName = settingObject[DESCRIPTION_NAME_KEY].toString();
// we need to check if the settings map has a value for this setting
QVariant variantValue;
if (!groupKey.isEmpty()) {
QVariant settingsMapGroupValue = _configMap.value(groupKey);
QVariant settingsMapGroupValue = valueForKeyPath(groupKey);
if (!settingsMapGroupValue.isNull()) {
variantValue = settingsMapGroupValue.toMap().value(settingName);
}
} else {
variantValue = _configMap.value(settingName);
variantValue = valueForKeyPath(settingName);
}
QJsonValue result;
// final check for inclusion
// either we include default values or we don't but this isn't a default value
if ((defaultSettingsInclusion == IncludeDefaultSettings) || variantValue.isValid()) {
QJsonValue result;
if (variantValue.isNull()) {
// no value for this setting, pass the default
if (settingObject.contains(SETTING_DEFAULT_KEY)) {
result = settingObject[SETTING_DEFAULT_KEY];
if (!variantValue.isValid()) {
// no value for this setting, pass the default
if (settingObject.contains(SETTING_DEFAULT_KEY)) {
result = settingObject[SETTING_DEFAULT_KEY];
} else {
// users are allowed not to provide a default for string values
// if so we set to the empty string
result = QString("");
}
} else {
// users are allowed not to provide a default for string values
// if so we set to the empty string
result = QString("");
result = QJsonValue::fromVariant(variantValue);
}
} else {
result = QJsonValue::fromVariant(variantValue);
}
if (!groupKey.isEmpty()) {
// this belongs in the group object
groupResponseObject[settingName] = result;
} else {
// this is a value that should be at the root
responseObject[settingName] = result;
if (!groupKey.isEmpty()) {
// this belongs in the group object
groupResponseObject[settingName] = result;
} else {
// this is a value that should be at the root
responseObject[settingName] = result;
}
}
}
}
@ -1108,7 +1448,6 @@ QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& ty
}
}
return responseObject;
}
@ -1140,6 +1479,8 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV
settingMap[key] = sanitizedValue;
}
}
} else if (newValue.isDouble()) {
settingMap[key] = newValue.toDouble();
} else if (newValue.isBool()) {
settingMap[key] = newValue.toBool();
} else if (newValue.isObject()) {
@ -1212,7 +1553,12 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson
return QJsonObject();
}
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject,
SettingsType settingsType) {
// take a write lock since we're about to overwrite settings in the config map
QWriteLocker locker(&_settingsLock);
static const QString SECURITY_ROOT_KEY = "security";
static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist";
static const QString BROADCASTING_KEY = "broadcasting";
@ -1222,6 +1568,8 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
auto& settingsVariant = _configMap.getConfig();
bool needRestart = false;
auto& filteredDescriptionArray = settingsType == DomainSettings ? _domainSettingsDescription : _contentSettingsDescription;
// Iterate on the setting groups
foreach(const QString& rootKey, postedObject.keys()) {
const QJsonValue& rootValue = postedObject[rootKey];
@ -1236,7 +1584,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
QJsonObject groupDescriptionObject;
// we need to check the description array to see if this is a root setting or a group setting
foreach(const QJsonValue& groupValue, _descriptionArray) {
foreach(const QJsonValue& groupValue, filteredDescriptionArray) {
if (groupValue.toObject()[DESCRIPTION_NAME_KEY] == rootKey) {
// we matched a group - keep this since we'll use it below to update the settings
groupDescriptionObject = groupValue.toObject();
@ -1257,7 +1605,9 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
foreach(const QJsonValue& groupValue, _descriptionArray) {
// find groups with root values (they don't have a group name)
QJsonObject groupObject = groupValue.toObject();
if (!groupObject.contains(DESCRIPTION_NAME_KEY)) {
// this is a group with root values - check if our setting is in here
matchingDescriptionObject = settingDescriptionFromGroup(groupObject, rootKey);
@ -1269,6 +1619,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
if (rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY &&
rootKey != SETTINGS_PATHS_KEY && rootKey != WIZARD_KEY) {
needRestart = true;
@ -1286,6 +1637,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
const QJsonValue& settingValue = rootValue.toObject()[settingKey];
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
if ((rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY &&
rootKey != DESCRIPTION_ROOT_KEY && rootKey != WIZARD_KEY) ||
settingKey == AC_SUBNET_WHITELIST_KEY) {
@ -1304,6 +1656,12 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
}
}
// we're done making changes to the config map, let go of our read lock
locker.unlock();
// store whatever the current config map is to file
persistToFile();
return needRestart;
}
@ -1330,6 +1688,9 @@ bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) {
}
void DomainServerSettingsManager::sortPermissions() {
// take a write lock since we're about to change the config map data
QWriteLocker locker(&_settingsLock);
// sort the permission-names
QVariant* standardPermissions = _configMap.valueForKeyPath(AGENT_STANDARD_PERMISSIONS_KEYPATH);
if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) {
@ -1366,11 +1727,15 @@ void DomainServerSettingsManager::persistToFile() {
QFile settingsFile(_configMap.getUserConfigFilename());
if (settingsFile.open(QIODevice::WriteOnly)) {
// take a read lock so we can grab the config and write it to file
QReadLocker locker(&_settingsLock);
settingsFile.write(QJsonDocument::fromVariant(_configMap.getConfig()).toJson());
} else {
qCritical("Could not write to JSON settings file. Unable to persist settings.");
// failed to write, reload whatever the current config state is
// with a write lock since we're about to overwrite the config map
QWriteLocker locker(&_settingsLock);
_configMap.loadConfig(_argumentList);
}
}

View file

@ -13,6 +13,7 @@
#define hifi_DomainServerSettingsManager_h
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkReply>
@ -26,8 +27,6 @@
const QString SETTINGS_PATHS_KEY = "paths";
const QString SETTINGS_PATH = "/settings";
const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions";
const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions";
const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions";
@ -35,9 +34,14 @@ const QString MAC_PERMISSIONS_KEYPATH = "security.mac_permissions";
const QString MACHINE_FINGERPRINT_PERMISSIONS_KEYPATH = "security.machine_fingerprint_permissions";
const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions";
const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens";
const QString AUTOMATIC_CONTENT_ARCHIVES_GROUP = "automatic_content_archives";
using GroupByUUIDKey = QPair<QUuid, QUuid>; // groupID, rankID
enum SettingsType {
DomainSettings,
ContentSettings
};
class DomainServerSettingsManager : public QObject {
Q_OBJECT
@ -46,11 +50,12 @@ public:
bool handleAuthenticatedHTTPRequest(HTTPConnection* connection, const QUrl& url);
void setupConfigMap(const QStringList& argumentList);
// each of the three methods in this group takes a read lock of _settingsLock
// and cannot be called when the a write lock is held by the same thread
QVariant valueOrDefaultValueForKeyPath(const QString& keyPath);
QVariantMap& getSettingsMap() { return _configMap.getConfig(); }
QVariantMap& getDescriptorsMap();
QVariant valueForKeyPath(const QString& keyPath);
bool containsKeyPath(const QString& keyPath) { return valueForKeyPath(keyPath).isValid(); }
// these give access to anonymous/localhost/logged-in settings from the domain-server settings page
bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name, 0); }
@ -105,6 +110,24 @@ public:
void debugDumpGroupsState();
enum SettingsRequestAuthentication { NotAuthenticated, Authenticated };
enum DomainSettingsInclusion { NoDomainSettings, IncludeDomainSettings };
enum ContentSettingsInclusion { NoContentSettings, IncludeContentSettings };
enum DefaultSettingsInclusion { NoDefaultSettings, IncludeDefaultSettings };
enum SettingsBackupFlag { NotForBackup, ForBackup };
/// thread safe method to retrieve a JSON representation of settings
QJsonObject settingsResponseObjectForType(const QString& typeValue,
SettingsRequestAuthentication authentication = NotAuthenticated,
DomainSettingsInclusion domainSettingsInclusion = IncludeDomainSettings,
ContentSettingsInclusion contentSettingsInclusion = IncludeContentSettings,
DefaultSettingsInclusion defaultSettingsInclusion = IncludeDefaultSettings,
SettingsBackupFlag settingsBackupFlag = NotForBackup);
/// thread safe method to restore settings from a JSON object
Q_INVOKABLE bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType);
bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType);
signals:
void updateNodePermissions();
void settingsUpdated();
@ -123,20 +146,28 @@ private slots:
private:
QStringList _argumentList;
QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false);
bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject);
QJsonArray filteredDescriptionArray(bool isContentSettings);
void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap,
const QJsonObject& settingDescription);
QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName);
void sortPermissions();
// you cannot be holding the _settingsLock when persisting to file from the same thread
// since it may take either a read lock or write lock and recursive locking doesn't allow a change in type
void persistToFile();
double _descriptionVersion;
QJsonArray _descriptionArray;
HifiConfigVariantMap _configMap;
void splitSettingsDescription();
friend class DomainServer;
double _descriptionVersion;
QJsonArray _descriptionArray;
QJsonArray _domainSettingsDescription;
QJsonArray _contentSettingsDescription;
QJsonObject _settingsMenuGroups;
// any method that calls valueForKeyPath on this _configMap must get a write lock it keeps until it
// is done with the returned QVariant*
HifiConfigVariantMap _configMap;
// these cause calls to metaverse's group api
void apiGetGroupID(const QString& groupName);
@ -171,6 +202,9 @@ private:
// keep track of answers to api queries about which users are in which groups
QHash<QString, QHash<QUuid, QUuid>> _groupMembership; // QHash<user-name, QHash<group-id, rank-id>>
/// guard read/write access from multiple threads to settings
QReadWriteLock _settingsLock { QReadWriteLock::Recursive };
};
#endif // hifi_DomainServerSettingsManager_h

View file

@ -0,0 +1,83 @@
//
// EntitiesBackupHandler.cpp
// domain-server/src
//
// Created by Clement Brisset on 2/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
//
#include "EntitiesBackupHandler.h"
#include <QDebug>
#include <quazip5/quazip.h>
#include <quazip5/quazipfile.h>
#include <OctreeDataUtils.h>
EntitiesBackupHandler::EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) :
_entitiesFilePath(entitiesFilePath),
_entitiesReplacementFilePath(entitiesReplacementFilePath)
{
}
static const QString ENTITIES_BACKUP_FILENAME = "models.json.gz";
void EntitiesBackupHandler::createBackup(const QString& backupName, QuaZip& zip) {
QFile entitiesFile { _entitiesFilePath };
if (entitiesFile.open(QIODevice::ReadOnly)) {
QuaZipFile zipFile { &zip };
if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ENTITIES_BACKUP_FILENAME, _entitiesFilePath))) {
qCritical().nospace() << "Failed to open " << ENTITIES_BACKUP_FILENAME << " for writing in zip";
return;
}
auto entityData = entitiesFile.readAll();
if (zipFile.write(entityData) != entityData.size()) {
qCritical() << "Failed to write entities file to backup";
zipFile.close();
return;
}
zipFile.close();
if (zipFile.getZipError() != UNZ_OK) {
qCritical().nospace() << "Failed to zip " << ENTITIES_BACKUP_FILENAME << ": " << zipFile.getZipError();
}
}
}
void EntitiesBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) {
if (!zip.setCurrentFile(ENTITIES_BACKUP_FILENAME)) {
qWarning() << "Failed to find" << ENTITIES_BACKUP_FILENAME << "while recovering backup";
return;
}
QuaZipFile zipFile { &zip };
if (!zipFile.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open" << ENTITIES_BACKUP_FILENAME << "in backup";
return;
}
auto rawData = zipFile.readAll();
zipFile.close();
OctreeUtils::RawEntityData data;
if (!data.readOctreeDataInfoFromData(rawData)) {
qCritical() << "Unable to parse octree data during backup recovery";
return;
}
data.resetIdAndVersion();
if (zipFile.getZipError() != UNZ_OK) {
qCritical().nospace() << "Failed to unzip " << ENTITIES_BACKUP_FILENAME << ": " << zipFile.getZipError();
return;
}
QFile entitiesFile { _entitiesReplacementFilePath };
if (entitiesFile.open(QIODevice::WriteOnly)) {
entitiesFile.write(data.toGzippedByteArray());
}
}

View file

@ -0,0 +1,47 @@
//
// EntitiesBackupHandler.h
// domain-server/src
//
// Created by Clement Brisset on 2/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
//
#ifndef hifi_EntitiesBackupHandler_h
#define hifi_EntitiesBackupHandler_h
#include "BackupHandler.h"
class EntitiesBackupHandler : public BackupHandlerInterface {
public:
EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath);
std::pair<bool, float> isAvailable(const QString& backupName) override { return { true, 1.0f }; }
std::pair<bool, float> getRecoveryStatus() override { return { false, 1.0f }; }
void loadBackup(const QString& backupName, QuaZip& zip) override {}
void loadingComplete() override {}
// Create a skeleton backup
void createBackup(const QString& backupName, QuaZip& zip) override;
// Recover from a full backup
void recoverBackup(const QString& backupName, QuaZip& zip) override;
// Delete a skeleton backup
void deleteBackup(const QString& backupName) override {}
// Create a full backup
void consolidateBackup(const QString& backupName, QuaZip& zip) override {}
bool isCorruptedBackup(const QString& backupName) override { return false; }
private:
QString _entitiesFilePath;
QString _entitiesReplacementFilePath;
};
#endif /* hifi_EntitiesBackupHandler_h */

View file

@ -22,22 +22,10 @@
#include "DomainServer.h"
int main(int argc, char* argv[]) {
disableQtBearerPoll(); // Fixes wifi ping spikes
QCoreApplication::setApplicationName(BuildInfo::DOMAIN_SERVER_NAME);
QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION);
QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN);
QCoreApplication::setApplicationVersion(BuildInfo::VERSION);
setupHifiApplication(BuildInfo::DOMAIN_SERVER_NAME);
Setting::init();
#ifndef WIN32
setvbuf(stdout, NULL, _IOLBF, 0);
#endif
qInstallMessageHandler(LogHandler::verboseMessageHandler);
qInfo() << "Starting.";
int currentExitCode = 0;
// use a do-while to handle domain-server restart

View file

@ -11,18 +11,13 @@
#include <QtCore/QCoreApplication>
#include <LogHandler.h>
#include <SharedUtil.h>
#include "IceServer.h"
int main(int argc, char* argv[]) {
#ifndef WIN32
setvbuf(stdout, NULL, _IOLBF, 0);
#endif
qInstallMessageHandler(LogHandler::verboseMessageHandler);
qInfo() << "Starting.";
setupHifiApplication("Ice Server");
IceServer iceServer(argc, argv);
return iceServer.exec();
}
}

View file

@ -2,7 +2,7 @@ set(TARGET_NAME interface)
project(${TARGET_NAME})
file(GLOB_RECURSE QML_SRC resources/qml/*.qml resources/qml/*.js)
add_custom_target(qml SOURCES ${QML_SRC})
add_custom_target(qmls SOURCES ${QML_SRC})
GroupSources("resources/qml")
function(JOIN VALUES GLUE OUTPUT)
@ -204,13 +204,14 @@ endif()
# link required hifi libraries
link_hifi_libraries(
shared octree ktx gpu gl procedural graphics render
shared task octree ktx gpu gl procedural graphics graphics-scripting render
pointers
recording fbx networking model-networking entities avatars trackers
audio audio-client animation script-engine physics
render-utils entities-renderer avatars-renderer ui auto-updater midi
render-utils entities-renderer avatars-renderer ui qml auto-updater midi
controllers plugins image trackers
ui-plugins display-plugins input-plugins
workload
${PLATFORM_GL_BACKEND}
)

View file

@ -88,8 +88,8 @@
]
},
{ "from": "Keyboard.W", "to": "Actions.LONGITUDINAL_FORWARD" },
{ "from": "Keyboard.S", "to": "Actions.LONGITUDINAL_BACKWARD" },
{ "from": "Keyboard.W", "when": "!Keyboard.Control", "to": "Actions.LONGITUDINAL_FORWARD" },
{ "from": "Keyboard.S", "when": "!Keyboard.Control", "to": "Actions.LONGITUDINAL_BACKWARD" },
{ "from": "Keyboard.C", "to": "Actions.VERTICAL_DOWN" },
{ "from": "Keyboard.E", "to": "Actions.VERTICAL_UP" },
{ "from": "Keyboard.Left", "when": "Keyboard.RightMouseButton", "to": "Actions.LATERAL_LEFT" },

View file

@ -0,0 +1,148 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by Fontastic.me</metadata>
<defs>
<font id="hifi-glyphs" horiz-adv-x="512">
<font-face font-family="hifi-glyphs" units-per-em="512" ascent="480" descent="-32"/>
<missing-glyph horiz-adv-x="512" />
<glyph glyph-name="hmd" unicode="&#98;" d="M381 139l-70 0c-18 0-30 17-40 33-4 6-11 16-15 18-4-2-10-12-15-18-11-15-23-33-43-33l-67 0c-53 0-97 42-97 95l0 45c0 53 44 96 97 96l250 0c53 0 96-43 96-96l0-45c0-52-43-95-96-95z m-125 77c16 0 26-15 36-30 5-7 15-22 19-22l70 0c39 0 71 32 71 70l0 45c0 39-32 70-71 70l-250 0c-39 0-71-31-71-70l0-45c0-38 32-70 71-70l67 0c6 0 16 14 22 23 10 14 20 29 35 29 1 0 1 0 1 0z"/>
<glyph glyph-name="2d-screen" unicode="&#99;" d="M395 386l-276 0c-33 0-60-28-60-61l0-116c0-33 27-62 60-62l127 0 0-27-80 0c-8 0-14-5-14-13 0-8 7-13 14-13l186 0c7 0 13 5 13 13 0 8-6 13-13 13l-81 0 0 27 124 0c33 0 60 29 60 62l0 116c0 33-27 61-60 61z m32-177c0-18-14-33-32-33l-134 0c-1 0-1 0-2 0-1 0-2 0-2 0l-138 0c-18 0-32 15-32 33l0 116c0 18 14 33 32 33l276 0c18 0 32-15 32-33z"/>
<glyph glyph-name="keyboard" unicode="&#100;" d="M375 250l-26 0 0 25 26 0z m-35 0l-27 0 0 25 27 0z m-36 0l-26 0 0 25 26 0z m-36 0l-26 0 0 25 26 0z m-35 0l-27 0 0 25 27 0z m-36 0l-26 0 0 25 26 0z m-36 0l-26 0 0 25 26 0z m225-1l17 0c7 0 13 6 13 13 0 8-6 14-13 14l-18 0m-262 0l-17 0c-7 0-13-6-13-14 0-7 6-13 13-13l18 0m251 39l-31 0 0 25 31 0z m-41 0l-32 0 0 25 32 0z m-42 0l-32 0 0 25 32 0z m-42 0l-32 0 0 25 32 0z m-42 0l-31 0 0 25 31 0z m-41 0l-32 0 0 25 32 0z m219 0l17 0c7 0 13 6 13 13 0 7-6 13-13 13l-18 0m-262 0l-17 0c-7 0-13-6-13-13 0-7 6-13 13-13l18 0m287-124l-315 0c-32 0-59 27-59 60l0 77c0 33 27 60 59 60l315 0c33 0 60-27 60-60l0-77c0-33-27-60-60-60z m-315 171c-18 0-33-15-33-34l0-77c0-19 15-34 33-34l315 0c19 0 34 16 34 34l0 77c0 19-15 34-34 34z m249-99l32 0 0-25-32 0z m-42 0l32 0 0-25-32 0z m-42 0l32 0 0-25-32 0z m-42 0l32 0 0-25-32 0z m-42 0l32 0 0-25-32 0z m-42 0l32 0 0-25-32 0z m251-25l17 0c7 0 13 6 13 13 0 7-6 13-13 13l-18 0m-262 0l-17 0c-7 0-13-6-13-13 0-7 6-13 13-13l18 0"/>
<glyph glyph-name="hand-controllers" unicode="&#101;" d="M141 268l-3 0c-7 0-13 5-13 13 0 7 6 13 13 13l3 0c7 0 13-6 13-13 0-8-6-13-13-13z m28 26l-3 0c-7 0-13 6-13 13 0 7 6 13 13 13l3 0c7 0 13-6 13-13 0-7-6-13-13-13z m-12-169l-11 0c-31 0-50 23-50 60l-10 143c0 34 27 61 60 61l11 0c33 0 60-27 60-60l0-1-10-144c0-36-20-59-50-59z m-11 238c-19 0-34-15-34-34l10-143c0-14 3-35 24-35l11 0c21 0 24 21 24 34l10 144c-1 19-16 34-34 34z m203-69c-7 0-13 5-13 13l0 2c0 8 6 13 13 13 7 0 13-5 13-13l0-2c0-7-6-13-13-13z m27-29c-8 0-13 6-13 13l0 3c0 7 5 13 13 13 7 0 13-6 13-13l0-3c0-7-6-13-13-13z m-11-140l-10 0c-31 0-51 23-51 60l-9 143c0 34 27 61 60 61l10 0c33 0 60-27 60-60l0-1-9-144c0-36-20-59-51-59z m-10 238c-19 0-34-15-34-34l9-143c0-14 4-35 25-35l11 0c21 0 24 21 24 34l9 144c0 19-15 34-33 34z"/>
<glyph glyph-name="headphones-mic" unicode="&#102;" d="M419 348l-22 0c-3 48-42 83-89 83l-105 0c-47 0-86-35-89-83l-20 0c-25 0-45-19-45-44l0-71c0-25 20-45 45-45l19 0c1-27 14-50 33-66-3-17 5-35 20-45l41-25c7-4 15-7 23-7 3 0 6 1 10 2 11 2 21 9 27 19 13 21 6 48-14 60l-41 26c-10 6-21 8-33 5-8-2-15-6-20-11-12 11-20 27-20 45l0 152c0 34 29 62 64 62l105 0c35 0 64-28 64-62l0-152c0-1-1-3-1-3l48 0c25 0 46 20 46 45l0 71c0 25-21 44-46 44z m-306-134l-19 0c-10 0-19 9-19 19l0 71c0 10 9 19 19 19l19 0z m61-90c3 4 7 6 11 7 2 1 3 1 4 1 4 0 7-1 9-3l41-25c8-5 11-16 6-24-3-4-7-7-11-8-5-1-10 0-14 2l-40 25c-8 6-11 16-6 25z m264 109c0-10-8-19-19-19l-22 0 0 109 22 0c11 0 19-9 19-19z"/>
<glyph glyph-name="gamepad" unicode="&#103;" d="M107 136c-10 0-20 3-29 10-46 37-8 131-4 141l1 1c1 4 3 7 5 11 16 30 37 73 81 73l182 0c51 0 71-47 87-85 5-10 44-101 4-138-28-26-67-1-102 22-21 13-42 26-56 26l-39 0c-13 0-33-13-53-27-25-16-52-34-77-34z m-10 141c-10-24-30-90-3-112 17-13 47 7 76 26 24 16 47 31 67 31l40 0c20 0 44-15 68-30 28-18 59-37 72-25 23 22 0 89-10 110-17 42-31 70-64 70l-182 0c-29 0-45-33-59-60-2-3-3-7-5-10z m247-36l-3 0c-7 0-13 6-13 13 0 7 6 13 13 13l3 0c7 0 13-6 13-13 0-7-6-13-13-13z m0 55l-3 0c-7 0-13 6-13 13 0 7 6 13 13 13l3 0c7 0 13-6 13-13 0-7-6-13-13-13z m-29-29l-2 0c-8 0-13 6-13 13 0 8 5 13 13 13l2 0c8 0 13-5 13-13 0-7-5-13-13-13z m57 0l-3 0c-7 0-13 6-13 13 0 8 6 13 13 13l3 0c7 0 13-5 13-13 0-7-6-13-13-13z m-172 26l-12 0 0 13c0 7-6 13-13 13-7 0-13-6-13-13l0-13-13 0c-7 0-13-6-13-13 0-7 6-13 13-13l13 0 0-12c0-8 6-14 13-14 7 0 13 6 13 14l0 12 12 0c8 0 13 6 13 13 0 7-5 13-13 13z"/>
<glyph glyph-name="headphones" unicode="&#104;" d="M141 168l0 152c0 35 28 65 63 65l106 0c34 0 62-30 62-65l0-151c0-1 1-3 0-4l48 0c25 0 47 21 47 46l0 70c0 25-22 45-47 45l-21 0c-4 47-42 83-89 83l-106 0c-47 0-86-36-89-83l-19 0c-25 0-45-20-45-45l0-70c0-25 20-46 45-46l45 0z m-63 43l0 70c0 11 7 19 18 19l19 0 0-108-19 0c-11 0-18 8-18 19z m362 0c0-11-9-19-20-19l-20 0 0 108 20 0c11 0 20-8 20-19z"/>
<glyph glyph-name="mic" unicode="&#105;" d="M318 370c0 33-26 59-59 59l-6 0c-33 0-59-26-59-59l0-105c0-33 26-59 59-59l6 0c33 0 59 26 59 59z m-25-103c0-19-15-34-34-34l-7 0c-19 0-34 15-34 34l0 104c0 19 15 34 34 34l7 0c19 0 34-15 34-34z m82 8c0 7-6 13-12 13-7 0-13-6-13-13 0-51-42-93-93-93-52 0-93 42-93 93 0 7-6 13-13 13-7 0-12-6-12-13 0-60 46-110 104-117l0-34-80 0c-8 0-14-6-14-14 0-7 6-13 14-13l186 0c7 0 13 6 13 13 0 8-6 14-13 14l-80 0 0 34c60 6 106 56 106 117z"/>
<glyph glyph-name="upload" unicode="&#106;" d="M330 193l-83 86-84-86 52 0 0-141 61 23 0 118z m-12 247c-39 0-76-15-105-41-23-21-40-49-47-80-9 3-19 4-29 4-53 0-97-43-97-97 0-54 44-98 97-98 1 0 19 0 45 0l0 29c-26 0-44 0-45 0-37 0-68 31-68 69 0 37 31 68 68 68 12 0 23-3 34-9l19-11 2 23c3 31 18 59 41 81 23 21 54 33 85 33 70 0 127-57 127-127 0-70-57-127-127-127 0 0-5 0-10 0l0-29c5 0 10 0 10 0 86 0 156 70 156 156 0 86-70 156-156 156z"/>
<glyph glyph-name="script" unicode="&#107;" d="M283 80l-150 0c-30 0-56 15-73 44-13 21-17 42-17 42l-3 15 91 0 0 252 315 0 0-238c1-7 5-58-21-87-13-14-29-21-50-21-42 0-63 23-73 41-6 11-9 21-10 29l-220 0c2-6 5-13 9-20 13-21 31-32 52-32l150 0c7 0 13-6 13-13 0-6-6-12-13-12z m-127 101l158 0 1-12c0 0 1-15 9-30 10-18 27-28 51-28 13 0 23 5 31 13 10 11 14 29 15 42 1 15 0 27 0 27l0 1 0 214-265 0z m225 168l-185 0c-8 0-14 6-14 13 0 8 6 14 14 14l185 0c8 0 14-6 14-14 0-7-6-13-14-13z m0-61l-185 0c-8 0-14 7-14 14 0 8 6 14 14 14l185 0c8 0 14-6 14-14 0-7-6-14-14-14z m0-60l-185 0c-8 0-14 6-14 14 0 7 6 13 14 13l185 0c8 0 14-6 14-13 0-8-6-14-14-14z"/>
<glyph glyph-name="text" unicode="&#108;" d="M220 134l-81 232c-1 2-3 4-6 4l-10 0c-3 0-5-2-6-4l-83-233c-1-2 0-5 1-7 1-1 3-3 6-3l16 0c3 0 5 3 6 5l27 79 74 0 27-79c1-2 4-5 7-5l16 0c2 0 4 2 5 4 2 1 2 4 1 7z m-120 102l26 73c1 2 1 3 2 5 0-2 1-3 2-4l24-74z m252 60c-10 12-25 18-44 18-17 0-35-5-53-14-3-2-4-6-3-9l5-14c1-2 2-3 4-4 2-1 4 0 6 1 14 8 28 12 41 12 11 0 19-3 23-10 5-7 8-18 8-33l0-4-23-1c-25 0-44-6-58-16-14-11-21-26-21-45 0-17 4-31 14-40 10-10 23-16 40-16 12 0 23 3 32 8 6 4 11 8 17 14l1-13c1-4 4-7 7-7l10 0c4 0 8 4 8 8l0 115c0 23-5 39-14 50z m-87-119c0 11 4 19 11 24 8 5 22 9 42 10l19 1 0-10c0-17-4-30-12-39-8-9-19-13-33-13-9 0-16 2-20 7-5 4-7 11-7 20z m186-105c-10 0-15 8-15 18 0 22 0 298 0 320 0 10 5 19 15 19 0 0 0 0 0 0 10 0 15-8 15-18 1-22 1-299 1-321 0-10-6-18-16-18 0 0 0 0 0 0z"/>
<glyph glyph-name="cube" unicode="&#109;" d="M452 421l-263 26c-3 0-6 0-8-2l0 0-126-88c-1-1-1-1-1-1-3-3-4-7-4-10l0-263c0-7 5-12 12-13l262-26c0 0 1 0 1 0 3 0 5 1 8 2l126 89c0 0 0 0 0 1 3 2 4 6 4 9l0 263c0 7-5 12-11 13z m-22-274l-2-1c0 0 0 0 0-1 0 0 0 0 0 0l-89-62 0 230 98 69 0-222c-1-6-2-9-7-13z m-227 272l211-21-92-65-222 22 86 61c1 0 1 0 1 0 0 0 0 0 0 0 3 2 7 4 11 4 2 0 4-1 5-1z m110-347l-237 23 0 236 237-23z"/>
<glyph glyph-name="sphere" unicode="&#110;" d="M418 415c-91 91-237 91-327 0-44-43-68-101-68-163 0-62 24-120 68-164 45-45 104-67 163-67 59 0 119 22 164 67 43 44 68 102 68 164-1 62-25 120-68 163z m-305-22c30 30 67 49 106 55-8-11-14-26-20-44-13-41-20-96-20-154 0-3 0-7 0-10 6-1 12-2 19-3 2 0 4 0 6 0 0 4 0 8 0 13 0 117 30 193 52 200 1 0 1 1 1 1 51 0 100-20 138-58 37-37 58-85 59-137-5-22-82-53-200-53-116 0-192 29-200 52 1 52 22 101 59 138z m283-283c-40-39-92-59-144-58-14 9-30 43-40 97l-25 1c3-19 7-39 12-54 5-16 11-30 17-40-38 7-74 25-103 54-30 30-49 67-56 107 11-7 25-13 42-18 42-14 97-21 155-21 58 0 113 7 154 21 18 5 32 12 43 19-7-41-26-78-55-108z"/>
<glyph glyph-name="zone" unicode="&#111;" d="M381 372c-3 7-9 11-17 11l-72 0 0-34 30 0-163-163 0 36-36 0 0-79 1 0c0-2 1-4 2-6 2-7 9-11 16-11l83 0 0 35-41 0 162 161 0-25 35 0 0 62c1 4 1 9 0 13z m-43-298c0 12 10 22 22 22 12 0 22-10 22-22 0-12-10-21-22-21-12 0-22 9-22 21z m-71 0c0 12 10 22 22 22 12 0 21-10 21-22 0-12-9-21-21-21-12 0-22 9-22 21z m-72 0c0 12 10 22 22 22 12 0 22-10 22-22 0-12-10-21-22-21-12 0-22 9-22 21z m-71 0c0 12 10 22 22 22 12 0 21-10 21-22 0-12-9-21-21-21-12 0-22 9-22 21z m-50-21c-6 0-11 2-15 6-4 4-7 10-7 15 0 6 3 12 7 16 4 4 9 6 15 6 6 0 11-2 15-6 4-4 7-10 7-16 0-5-2-11-7-15-4-4-9-6-15-6z m-22 93c0 12 10 22 22 22 12 0 22-10 22-22 0-12-10-22-22-22-12 0-22 10-22 22z m0 71c0 12 10 22 22 22 12 0 22-10 22-22 0-12-10-21-22-21-12 0-22 9-22 21z m0 72c0 12 10 22 22 22 12 0 22-10 22-22 0-12-10-22-22-22-12 0-22 10-22 22z m0 71c0 12 10 22 22 22 12 0 22-10 22-22 0-12-10-21-22-21-12 0-22 9-22 21z m286 72c0 12 10 22 22 22 12 0 22-10 22-22 0-12-10-22-22-22-12 0-22 10-22 22z m-71 0c0 12 10 22 22 22 12 0 21-10 21-22 0-12-9-22-21-22-12 0-22 10-22 22z m-72 0c0 12 10 22 22 22 12 0 22-10 22-22 0-12-10-22-22-22-12 0-22 10-22 22z m-71 0c0 12 10 22 22 22 12 0 21-10 21-22 0-12-9-22-21-22-12 0-22 10-22 22z m308-22c-6 0-12 3-16 7-4 4-6 9-6 15 0 6 2 11 6 15 4 4 10 7 16 7 5 0 11-3 15-7 4-4 6-9 6-15 0-6-2-11-6-15-4-4-10-7-15-7z m-22-264c0 12 10 22 22 22 12 0 21-10 21-22 0-12-9-22-21-22-12 0-22 10-22 22z m0 71c0 12 10 22 22 22 12 0 21-10 21-22 0-12-9-21-21-21-12 0-22 9-22 21z m0 72c0 12 10 22 22 22 12 0 21-10 21-22 0-12-9-22-21-22-12 0-22 10-22 22z m0 71c0 12 10 22 22 22 12 0 21-10 21-22 0-12-9-21-21-21-12 0-22 9-22 21z"/>
<glyph glyph-name="light" unicode="&#112;" d="M298 259c0-21-18-38-39-38-21 0-38 17-38 38 0 22 17 39 38 39 21 0 39-17 39-39z m-39-109c-60 0-109 49-109 109 0 61 49 110 109 110 61 0 110-49 110-110 0-60-50-109-110-109z m0 190c-44 0-80-36-80-81 0-44 36-80 80-80 45 0 81 36 81 80 0 45-36 81-81 81z m155-97c-8 0-14 7-14 15 0 8 6 14 14 14 17 0 35 0 52 0 0 0 0 0 0 0 8 0 14-6 14-14 0-8-6-14-14-14-17 0-35 0-52-1 0 0 0 0 0 0z m-308 0c-17 1-35 1-52 1-8 0-14 6-14 14 0 8 7 14 14 14 0 0 0 0 1 0 17 0 34 0 51 0 8 0 14-7 14-14 0-8-6-15-14-15z m263 109c-3 0-7 2-10 5-5 5-5 14 0 20 12 12 24 24 36 36 6 6 15 6 21 1 5-6 5-15 0-21-12-12-25-24-37-36-3-3-6-5-10-5z m-255-254c-3 0-7 2-10 5-5 5-5 14 0 20 13 12 25 24 37 36 6 5 15 5 20 0 6-6 6-15 0-20-12-13-24-25-37-37-2-2-6-4-10-4z m146 300c-8 0-14 6-14 14 0 17 0 34 0 52 0 7 6 14 14 14 0 0 0 0 0 0 8 0 14-6 14-14 0-18 1-35 1-52 0-8-7-14-15-14z m0-360c0 0 0 0 0 0-8 0-14 6-14 14 0 17 0 35 0 52 0 8 6 14 14 14 0 0 0 0 0 0 8 0 15-7 15-14 0-18-1-35-1-52 0-8-6-14-14-14z m-109 315c-3 0-7 1-10 4-12 12-24 24-36 36-6 6-6 15-1 20 6 6 15 6 21 0 12-12 24-24 36-36 6-6 6-15 0-20-2-3-6-4-10-4z m254-255c-3 0-7 1-10 4-12 12-24 25-36 37-5 6-5 15 0 20 6 6 15 6 20 0 13-12 25-25 37-37 5-5 5-14-1-20-2-3-6-4-10-4z"/>
<glyph glyph-name="web" unicode="&#113;" d="M438 390c0 8-6 15-14 15l-333 0c-8 0-15-7-15-15l0-298c0-8 7-14 15-14l333 0c8 0 14 6 14 14z m-219-8l172 0c8 0 15-7 15-16 0-9-7-16-15-16l-172 0c-8 0-15 7-15 16 0 9 7 16 15 16z m-47 1c9 0 16-7 16-16 0-10-7-17-16-17-10 0-17 7-17 17 0 9 7 16 17 16z m-51 0c9 0 17-7 17-16 0-10-8-17-17-17-9 0-17 7-17 17 0 9 8 16 17 16z m291-276l-308 0 0 219 308 0z m-250 84l-15 0-20 60 13 0 14-45 15 45 13 0 15-45 14 45 13 0-20-60-14 0-14 41z m137 25l-46 0c0-5 2-8 6-11 3-2 8-4 12-4 8 0 13 3 17 7l7-8c-6-6-14-9-25-9-8 0-15 2-21 8-6 5-9 13-9 22 0 9 3 17 9 22 6 6 13 9 21 9 8 0 15-3 21-8 6-5 8-11 8-20z m-46 9l34 0c0 5-2 9-5 12-3 3-7 4-11 4-5 0-9-1-13-4-3-3-5-7-5-12z m101 27c8 0 14-3 20-9 6-5 9-12 9-22 0-9-3-16-9-22-5-6-12-8-19-8-8 0-15 3-21 9l0-9-12 0 0 83 12 0 0-34c5 8 12 12 20 12z m-20-31c0-6 2-10 5-14 4-4 8-5 13-5 5 0 9 1 13 5 3 4 5 8 5 14 0 6-2 10-5 14-4 4-8 6-13 6-5 0-9-2-13-6-3-4-5-8-5-14z"/>
<glyph glyph-name="web-2" unicode="&#114;" d="M438 390c0 8-6 15-14 15l-333 0c-8 0-15-7-15-15l0-298c0-8 7-14 15-14l333 0c8 0 14 6 14 14z m-219-8l172 0c8 0 15-7 15-16 0-9-7-16-15-16l-172 0c-8 0-15 7-15 16 0 9 7 16 15 16z m-47 1c9 0 16-7 16-16 0-10-7-17-16-17-10 0-17 7-17 17 0 9 7 16 17 16z m-51 0c9 0 17-7 17-16 0-10-8-17-17-17-9 0-17 7-17 17 0 9 8 16 17 16z m291-276l-308 0 0 219 308 0z"/>
<glyph glyph-name="edit" unicode="&#115;" d="M196 214c-27-27-54-54-81-81-6 6-13 13-19 19 27 27 54 54 81 81l-22 21c-31-31-61-62-92-92-7-7-11-13-12-22-3-25-7-50-11-76 3 0 4 0 6 0 24 5 48 10 72 15 5 1 10 4 13 7 32 32 64 64 96 96z m126 207c10 10 21 21 33 32 4 4 10 4 14 0 19-19 38-38 57-57 4-5 4-10 0-15-11-11-22-22-34-33-23 24-46 48-70 73z m23-181c-5-1-8 0-11 3-8 8-15 15-23 23 18 18 37 37 55 55 2 2 4 4 4 4-24 25-47 49-71 74-2-2-3-4-5-6-18-18-37-37-55-55-34 34-67 67-101 101-2 2-5 5-8 7-18 14-42 12-57-5-15-17-14-42 2-58 50-51 101-101 151-151 17-16 33-33 50-49 2-3 3-5 2-8-2-8-4-15-4-23-4-48 27-90 73-102 20-5 39-4 58 4-1 2-3 3-5 4-14 14-28 28-42 42-10 11-11 26-2 36 8 8 16 16 24 24 11 10 24 10 35 0 2-1 4-4 6-6 15-14 29-28 43-42 0 0 1 0 2 1 1 8 3 16 4 24 7 69-59 123-125 103z m-243 162c-7 0-14 6-14 14 0 8 6 14 14 14 8 0 15-6 15-14 0-7-7-14-15-14z m198-46c6-6 13-12 19-19-13-13-26-26-39-39-7 6-13 12-20 19 14 13 27 26 40 39z"/>
<glyph glyph-name="market" unicode="&#116;" d="M88 370c3 0 7 0 10 0 6 0 11-1 15-2 9-2 16-8 20-16 3-5 4-10 6-15 2-6 3-13 5-19 3-10 5-19 8-27 2-6 3-13 5-19 3-7 5-15 7-23 3-9 6-19 9-30 0-2 1-4 1-6 2-8 5-17 9-24 3-7 8-12 13-15 6-3 13-5 22-6 2 0 5 0 7 0l21 0 57 0c25 0 50 0 75 0 12 0 25 0 37 0 1 0 1 0 2 0 6 0 13 0 17 2 5 3 7 9 9 16l3 10c0 0 0 0 0 0 0 2 1 3 1 3 5 20 11 40 17 60 1 4 2 9 4 13 2 8 4 16 7 24 2 7 4 16 0 22-3 4-9 5-14 5-6 0-188-2-284-4l-4 0-5 22c0 3-1 6-2 9 0 3-1 5-1 7-1 2-1 3-1 4-1 4-2 7-3 11-2 6-6 12-11 17-5 5-11 8-17 9-3 1-6 1-10 1-5 1-9 1-14 1-19 0-38 0-59 0-1 0-3 0-4 0-3 0-7 0-10-1-3 0-6-2-8-4-3-5-4-11-2-17 2-5 6-6 9-7 4-1 8-1 13-1 5 0 10 0 16 0l16 0c3 0 5 0 8 0z m353-78l-26-92-6-4-2 0c-2 0-21 0-55 0-50 0-118-1-125-1l-1-1 0 0c-6 0-12 2-16 6-4 4-6 10-8 16-3 12-6 25-10 39-1 4-2 8-3 12-2 4-3 8-4 13l-4 12z m-194-164c-9 0-15-2-20-7-5-5-8-11-8-20 0-8 3-16 8-21 5-5 13-8 20-8 17 0 30 12 30 28 0 8-3 15-9 20-5 6-12 8-21 8z m139 0c-9 0-16-2-21-6-5-6-8-13-8-21 0-9 3-16 8-21 5-6 12-8 21-8 16 0 29 13 29 29 0 8-3 14-8 20-5 5-13 7-21 7z"/>
<glyph glyph-name="directory" unicode="&#117;" d="M432 451l-99-38c-2 0-3-1-4-1-3 1-5 2-8 2l-116 38c-8 3-15 6-30-1l-91-35c-17-5-32-16-32-31l0-303c0-15 14-27 32-27l99 38c3 1 6 2 9 4 1-1 3-2 4-2 3-1 5-2 7-3 0 0 1 0 1-1 0 0 1 0 1 0l116-38 0 0c8-1 12-1 21 3l90 34c13 6 32 16 32 31l0 304c0 14-14 26-32 26z m-351-371c-1 0-1 1-1 2l0 303c0 2 5 6 14 9l1 0 1 1 89 34c1-1 0-1 0-2l0-303c0-1-3-4-16-10z m356 42c-1-2-5-5-17-11l-90-34c0 1-1 1-1 2l0 304c0 1 5 5 14 8l1 0 1 1 91 34c0 0 1-1 1-1z"/>
<glyph glyph-name="menu" unicode="&#118;" d="M257 22c-60 0-119 22-164 67-44 44-68 102-68 164 0 62 24 120 68 163 90 91 237 91 327 0 44-43 68-101 68-163 0-62-24-120-68-164-45-45-104-67-163-67z m0 431c-52 0-103-20-142-59-38-38-58-88-58-141 0-54 20-104 58-142 78-78 205-78 283 0 38 38 59 88 59 142 0 53-21 103-59 141-39 39-90 59-141 59z m101-133l-203 0c-8 0-15 7-15 15 0 8 7 15 15 15l203 0c8 0 14-7 14-15 0-8-6-15-14-15z m0-84l-203 0c-8 0-15 7-15 15 0 8 7 15 15 15l203 0c8 0 14-7 14-15 0-8-6-15-14-15z m0-81l-203 0c-8 0-15 7-15 15 0 8 7 14 15 14l203 0c8 0 14-6 14-14 0-8-6-15-14-15z"/>
<glyph glyph-name="close" unicode="&#119;" d="M258 19c-59 0-118 23-163 68-44 43-68 101-68 163 0 62 24 120 68 164 90 90 237 90 327 0 44-44 68-102 68-164 0-62-24-120-68-163-45-45-104-68-164-68z m0 431c-51 0-102-19-141-58-38-38-59-88-59-142 0-53 21-103 59-141 78-78 205-78 283 0 38 38 58 88 58 141 0 54-20 104-58 142-39 39-90 58-142 58z m25-200l67 67c7 7 7 18 0 25-7 7-18 7-25 0l-67-67-66 67c-7 7-18 7-25 0-7-7-7-18 0-25l66-67-66-66c-7-7-7-18 0-25 7-7 18-7 25 0l66 66 67-66c7-7 18-7 25 0 7 7 7 18 0 25z"/>
<glyph glyph-name="close-inverted" unicode="&#120;" d="M400 388c-39 39-90 59-142 59-51 0-102-20-141-59-38-37-59-88-59-141 0-53 21-104 59-141 78-78 205-78 283 0 38 37 58 88 58 141 0 53-20 104-58 141z m-45-208c8-9 8-22 0-30-8-8-21-8-29 0l-68 67-67-67c-8-8-21-8-29 0-9 8-9 21 0 30l67 67-67 67c-9 8-9 22 0 30 8 8 21 8 29 0l67-68 68 68c8 8 21 8 29 0 8-9 8-22 0-30l-67-67z"/>
<glyph glyph-name="pin" unicode="&#121;" d="M304 144c1 16-2 31-8 45l97 115 25-8c14-4 29 2 36 14 8 13 6 29-5 39l-116 116c-10 11-26 13-39 5-12-8-18-23-13-37l8-25-115-102c-33 11-72 2-98-24-12-12-12-33 0-45l65-65-83-82c-6-7-6-17 0-23 6-6 16-6 23 0l82 83 66-66c13-13 33-13 45 0 17 17 28 38 30 60z m-205 115c21 22 55 26 81 9l145 130-14 45 116-116-45 13-126-149c25-28 18-62-4-85z"/>
<glyph glyph-name="pin-inverted" unicode="&#122;" d="M297 160c2 14 0 27-6 39l92 106 21-5c13-4 26 2 33 13 8 12 6 26-3 35l-98 99c-9 8-24 10-35 2-11-7-17-21-13-33l6-22-106-96c-30 9-64 0-88-24-12-12-13-30-2-41l55-55-77-76c-5-6-6-15-1-20 6-6 15-5 20 1l77 76 56-56c11-11 29-10 41 2 15 15 25 35 28 55z"/>
<glyph glyph-name="resize-handle" unicode="&#65;" d="M262 175l70 71c7 7 18 7 25 0 7-7 7-18 0-25l-70-71c-7-7-18-7-25 0-7 7-7 18 0 25m-101 0l175 175c7 7 18 7 25 0 7-7 7-18 0-25l-175-175c-7-7-18-7-25 0-7 7-7 18 0 25"/>
<glyph glyph-name="diclosure-expand" unicode="&#66;" d="M239 187c-3 0-6 1-8 3-5 5-5 12 0 17l42 42-43 44c-5 4-5 12 0 17 4 4 12 4 17 0l60-61-60-59c-2-2-5-3-8-3z"/>
<glyph glyph-name="reload-small" unicode="&#97;" d="M334 253c-9 0-18-7-18-16-3-27-25-47-52-47-29 0-52 24-52 52 0 11 2 27 14 38 6 5 14 9 24 12-5-7-4-16 2-22 3-3 8-4 12-4 5 0 9 1 13 5l28 28c1 2 3 4 3 7 3 6 1 13-3 18l-29 29c-3 3-7 5-12 5-5 0-9-2-12-5-4-3-5-8-5-12 0-5 1-9 5-13l0 0c-20-3-37-11-50-23-16-16-25-38-25-63 0-47 39-86 86-86 45 0 82 33 87 78 1 9-6 18-16 19z"/>
<glyph glyph-name="close-small" unicode="&#67;" d="M291 259l43 44c8 7 8 19 0 27-7 7-19 7-26 0l-44-44-44 44c-7 7-19 7-26 0-8-8-8-20 0-27l43-44-43-43c-8-8-8-20 0-27 7-7 19-7 26 0l44 44 44-44c7-7 19-7 26 0 8 8 8 19 0 27z"/>
<glyph glyph-name="backward" unicode="&#69;" d="M292 349c-5 3-12 3-18-1l-94-71c-4-3-7-8-7-13 0-5 2-10 6-14l95-80c3-2 7-4 11-4 2 0 5 1 7 2 6 3 10 9 10 15l0 151c0 6-4 12-10 15"/>
<glyph glyph-name="reload" unicode="&#70;" d="M365 261c-9 1-17-5-18-15-4-45-43-80-89-80-49 0-89 40-89 89 0 19 4 45 25 65 16 15 39 23 68 25l-15-16c-6-6-6-17 0-24 4-3 8-4 12-4 4 0 9 1 12 5l43 44c2 2 3 4 4 6 2 6 1 13-4 18l-44 44c-6 6-17 6-23 0-7-7-7-17 0-24l15-15c-38-2-69-14-91-35-23-21-36-53-36-88 0-68 55-123 123-123 64 0 116 47 122 110 1 9-5 18-15 18"/>
<glyph glyph-name="minimize" unicode="&#73;" d="M154 282l198 0c10 0 18-8 18-18 0-10-8-18-18-18l-198 0c-10 0-18 8-18 18 0 10 8 18 18 18"/>
<glyph glyph-name="maximize" unicode="&#74;" d="M157 244l77 0 0-75c0-9 8-17 17-17 9 0 17 8 17 17l0 75 75 0c10 0 17 8 17 17 0 10-7 18-17 18l-75 0 0 76c0 10-8 17-17 17-9 0-17-7-17-17l0-76-77 0c-10 0-17-8-17-18 0-9 8-17 17-17z"/>
<glyph glyph-name="maximize-inverted" unicode="&#75;" d="M251 434c-96 0-173-78-173-173 0-96 77-173 173-173 95 0 173 77 173 173 0 95-78 173-173 173z m93-190l-77 0 0-76c0-10-7-17-16-17-9 0-16 7-16 17l0 76-77 0c-10 0-17 8-17 17 0 9 7 17 17 17l77 0 0 76c0 9 7 17 16 17 9 0 16-8 16-17l0-76 77 0c9 0 17-8 17-17 0-9-8-17-17-17z"/>
<glyph glyph-name="disclosure-button-expand" unicode="&#76;" d="M264 202l-60 59c-4 5-4 12 0 17 5 4 13 4 17 0l43-43 43 44c4 4 12 4 17 0 4-5 4-13 0-17z m90-79l-188 0c-16 0-29 13-29 29l0 188c0 16 13 29 29 29l188 0c16 0 29-13 29-29l0-188c0-16-13-29-29-29z m-188 222c-3 0-5-2-5-5l0-188c0-3 2-5 5-5l188 0c3 0 5 2 5 5l0 188c0 3-2 5-5 5z"/>
<glyph glyph-name="disclosure-button-collapse" unicode="&#77;" d="M264 290l-60-59c-4-5-4-12 0-17 5-4 13-4 17 0l43 43 43-44c4-4 12-4 17 0 4 5 4 13 0 17z m119 50l0-188c0-16-13-29-29-29l-188 0c-16 0-29 13-29 29l0 188c0 16 13 29 29 29l188 0c16 0 29-13 29-29z m-29-193c3 0 5 2 5 5l0 188c0 3-2 5-5 5l-188 0c-3 0-5-2-5-5l0-188c0-3 2-5 5-5z"/>
<glyph glyph-name="script-stop" unicode="&#78;" d="M298 79l-145 0c-29 0-54 14-71 42-13 20-17 41-17 41l-3 16 267 0 1-13c0 0 1-15 9-29 9-17 26-26 48-26 13 0 23 4 30 12 16 18 16 54 15 66l0 1 0 205-270 0c-7 0-13 7-13 14 0 7 6 14 13 14l294 0 0-232c1-7 5-57-20-86-13-13-29-20-49-20-41 0-61 22-71 41-6 9-9 18-10 27l-211 0c2-7 5-12 9-18 12-20 29-31 49-31l145 0c8 0 13-5 13-12 0-7-5-12-13-12z m95 258l-112 0c-7 0-14 7-14 14 0 8 7 15 14 15l112 0c8 0 14-7 14-15 0-8-6-14-14-14z m0-58l-143 0c-7 0-14 6-14 14 0 8 7 14 14 14l143 0c8 0 14-6 14-14 0-8-6-14-14-14z m0-58l-112 0c-7 0-14 6-14 14 0 8 7 14 14 14l112 0c8 0 14-6 14-14 0-8-6-14-14-14z m-204 72l42 42c7 7 7 19 0 26-7 7-18 7-25 0l-43-42-42 42c-7 7-18 7-26 0-7-7-7-19 0-26l43-42-43-42c-7-7-7-19 0-26 8-7 19-7 26 0l42 42 43-42c7-7 18-7 25 0 7 7 7 19 0 26z"/>
<glyph glyph-name="script-reload" unicode="&#79;" d="M236 295c-10 1-18-6-19-15-2-26-24-45-50-45-28 0-50 22-50 50 0 10 2 25 14 36 5 6 13 10 23 12-4-7-4-15 2-21 3-3 7-5 12-5 4 0 9 2 12 5l27 28c1 2 3 4 3 6 3 7 2 14-3 18l-28 28c-3 3-7 5-11 5-5 0-9-2-12-5-3-3-5-7-5-12 0-4 2-9 5-12l1-1c-20-2-37-10-49-22-16-14-25-36-25-60 0-46 38-84 84-84 43 0 79 33 83 76 1 9-5 17-14 18z m62-216l-145 0c-30 0-55 15-72 43-12 20-16 40-16 41l-3 15 267 0 0-12c0 0 1-15 9-29 10-18 26-26 49-26 13 0 22 4 29 11 16 19 16 55 14 67l0 1 0 207-215 0c-7 0-12 5-12 12 0 8 5 13 12 13l241 0 0-231c1-7 5-57-21-86-12-13-28-20-48-20-41 0-62 22-72 40-5 10-8 20-10 27l-210 0c2-5 5-11 8-17 13-20 29-29 50-29l145 0c7 0 13-6 13-14 0-7-6-13-13-13z m95 259l-139 0c-8 0-14 6-14 14 0 8 6 14 14 14l139 0c7 0 14-6 14-14 0-8-7-14-14-14z m0-58l-103 0c-7 0-14 6-14 14 0 8 7 14 14 14l103 0c7 0 14-6 14-14 0-8-7-14-14-14z m0-58l-111 0c-7 0-14 6-14 14 0 8 7 14 14 14l111 0c7 0 14-6 14-14 0-8-7-14-14-14z"/>
<glyph glyph-name="script-run" unicode="&#80;" d="M209 309l-79 60c-5 3-11 4-16 1-4-2-8-7-8-13l0-126c0-6 4-11 9-14 2-1 4-1 6-1 3 0 7 1 9 3l80 68c3 3 5 7 5 11 0 5-2 9-6 11m89-232l-145 0c-30 0-55 16-72 44-12 20-16 40-16 41l-3 16 267 0 0-13c0 0 1-14 9-29 10-17 26-26 49-26 13 0 22 4 29 12 16 18 16 54 14 66l0 1 0 206-269 0c-7 0-13 6-13 13 0 7 6 13 13 13l295 0 0-230c1-8 5-57-21-86-12-14-28-21-48-21-41 0-62 23-72 41-5 10-8 19-10 28l-210 0c2-7 5-12 8-18 13-20 29-31 50-31l145 0c7 0 13-7 13-14 0-7-6-13-13-13z m95 261l-180 0c-7 0-14 6-14 14 0 8 7 14 14 14l180 0c7 0 14-6 14-14 0-8-7-14-14-14z m0-59l-135 0c-8 0-14 7-14 15 0 7 6 14 14 14l135 0c7 0 14-7 14-14 0-8-7-15-14-15z m0-58l-180 0c-7 0-14 7-14 14 0 8 7 15 14 15l180 0c7 0 14-7 14-15 0-8-7-14-14-14z"/>
<glyph glyph-name="script-new" unicode="&#81;" d="M298 80l-145 0c-30 0-55 15-72 43-12 20-16 40-16 41l-3 16 267 0 0-12c0-1 1-15 9-29 10-18 26-27 49-27 13 0 22 4 29 12 16 18 16 54 14 66l0 1 0 206-269 0c-7 0-13 6-13 13 0 7 6 13 13 13l295 0 0-230c1-8 5-57-21-86-12-14-28-21-48-21-41 0-62 23-72 41-5 10-8 19-10 28l-210 0c2-7 5-12 8-18 13-20 29-30 50-30l145 0c7 0 13-7 13-14 0-7-6-13-13-13z m95 260l-180 0c-7 0-14 6-14 14 0 8 7 14 14 14l180 0c7 0 14-6 14-14 0-8-7-14-14-14z m0-59l-135 0c-8 0-14 7-14 15 0 7 6 14 14 14l135 0c7 0 14-7 14-14 0-8-7-15-14-15z m0-58l-180 0c-7 0-14 7-14 14 0 8 7 15 14 15l180 0c7 0 14-7 14-15 0-7-7-14-14-14z m-250 90l0 53c0 9-7 16-16 16-9 0-16-7-16-16l0-53-54 0c-9 0-16-8-16-17 0-9 8-17 16-17l54 0 0-53c0-9 7-16 16-16 9 0 16 8 16 17l0 52 54 0c9 0 16 8 16 17 0 9-8 16-17 16z"/>
<glyph glyph-name="hifi-forum" unicode="&#50;" d="M265 410c-83 0-150-68-150-150 0-24 6-47 16-67l-27-79 80 20c23-16 51-25 81-25 83 0 150 68 150 150 0 83-67 151-150 151z m38-248c-9 0-17 7-17 17 0 7 4 14 12 16l0 46-74 33 0-56c7-2 12-8 12-16 0-10-7-18-17-18-10 0-19 8-19 18 0 7 6 13 10 16l0 111c-4 2-10 9-10 16 0 10 9 17 19 17 9 0 17-7 17-17 0-8-5-14-12-16l0-41 74-33 0 51c-8 3-12 9-12 16 0 10 7 18 17 18 10 0 17-8 17-18 0-7-5-14-12-16l0-110c7-3 12-9 12-17 0-10-7-17-17-17z"/>
<glyph glyph-name="hifi-logo-small" unicode="&#83;" d="M374 374c-32 32-74 49-119 49-46 0-88-17-120-49-32-32-49-74-49-119 0-45 17-87 49-118 32-32 74-49 119-49 45 0 88 17 119 49 32 32 49 73 49 118 1 45-17 87-48 119z m-17-221c-28-28-65-43-103-43-39 0-75 15-103 43-27 27-42 64-42 102 0 39 15 75 42 103 28 28 64 43 103 43 38 0 75-15 103-43 27-28 42-64 42-103 0-39-15-76-42-102z m-145 47c-5 0-9 3-9 6l0 126c0 3 4 7 9 7 6 0 10-4 10-7l0-125c0-4-4-7-10-7z m0 118c-5 0-10 2-14 6-7 7-7 20 0 27 5 5 9 6 14 6 6 0 11-2 14-6 4-4 5-8 5-14 0-5-2-10-5-13-4-4-8-6-14-6z m0-144c-5 0-10 2-14 5-4 5-5 9-5 14 0 6 2 11 5 14 5 4 9 5 14 5 6 0 11-2 14-5 4-4 5-8 5-14 0-5-2-10-5-14-4-3-8-5-14-5z m85 2c-6 0-10 3-10 7l0 121c0 4 4 7 10 7 5 0 9-3 9-7l0-121c0-5-4-7-9-7z m0 120c-6 0-11 2-14 5-8 8-8 20 0 28 4 4 8 5 14 5 5 0 10-2 14-5 4-4 5-9 5-14 0-5-2-11-5-14-4-3-9-5-14-5z m0-144c-6 0-11 2-14 5-8 7-8 20 0 28 4 4 8 5 14 5 5 0 10-2 14-5 4-5 5-9 5-14 0-5-2-11-5-14-4-3-9-5-14-5z m1 73l-85 40 1 18 86-40z"/>
<glyph glyph-name="avatar-1" unicode="&#84;" d="M293 71c-1 0-1 0-1 0-14-1-14-1-16 12-6 43-11 86-16 128-1 2-1 4-1 6-3 0-6 0-9 0-2-10-3-20-4-31-4-36-8-72-12-109-1-7-2-8-9-8-2 0-5 0-7 0 0 74 1 181 1 254-1-1-33 0-44 1-15 0-62 1-79 1-8 0-14 3-18 10-1 2-2 4-4 7 8 0 15 0 22 1 35 1 99 8 100 12 14 11 23 10 36 10 15 0 31 0 46-1 11 0 24 3 37-10 20-10 81-10 123-11 0 0 1 0 1 0-4-12-12-17-25-17-29-1-77-3-127-1 2-73 4-181 6-254z m-32 371c16-6 14-20 13-32 0-5-1-10-1-14-2-11-10-18-20-18-11 0-19 8-20 18-1 6-1 13-2 20-1 7 3 13 11 15 10 3 10 3 19 11z"/>
<glyph glyph-name="placemark" unicode="&#85;" d="M134 98c31-32 73-49 119-49l1 0c45 0 86 16 117 47 31 30 48 71 48 114 1 46-16 88-46 120-9 9-20 17-31 24-3-7-6-15-10-22 5-4 11-8 16-13 15-14 28-32 33-46l1-1c0-1 0-2-1-3l-1 0c-12-7-25-10-38-12-2-1-4-1-7-2l-1 0c0 0-1 0-2 1 0 0-1 1-1 1l0 1c-3 15-6 31-12 46l-7-15c4-11 6-21 8-32l0-1c0-1 0-1 0-2-1-1-1-1-2-1l-1 0-55-3-4 0c0 0-1 0-1 0-1 1-1 2-1 2l0 74c-5 9-8 17-11 26 0-1 0-1 0-1l-1 0 0-97c0 0 0-1 0-2 0-1-1-2-2-2l-1 0c0 0-1 0-1 0l-51 3c-1 0-2 0-2 1-1 1-1 2 0 2l0 1c5 27 13 58 35 83 6 6 12 13 20 16 0 0 1 0 1 0-3 7-6 15-9 22-37-4-71-20-99-47-30-30-47-70-48-113-1-46 16-88 47-120z m198 72c0 4 1 7 1 10l0 0c0 7 1 14 1 21 0 9 0 18 0 27 0 3 0 6 0 9l0 1c-1 1-1 6 5 7 11 3 23 5 34 8l1 0c3 1 7 2 11 2l1 1c1 0 3-1 3-2l0-1c0-1 1-1 1-2 1-3 1-5 2-8 3-14 4-27 3-40-2-12-8-21-18-25-8-3-17-5-25-8-3 0-7-1-10-2-1-1-3-1-4-1-1 0-1 0-2 0l-1-1c0 0 0 0 0 0-1 0-2 1-2 1-1 1-1 1-1 2z m2-17c12 4 23 7 35 10l14 4c1 0 2 0 2-1 1 0 1-2 1-2l-1-2c-7-18-17-34-30-47-13-13-28-24-46-31l-2-1c0 0-1 0-1 0-1 0-1 0-2 0 0 1-1 2-1 3l1 1c0 1 0 1 0 1 0 1 0 2 1 3 6 9 15 24 22 55 1 3 2 6 7 7z m-78 83c0 1 1 3 2 3l3 0 41 2 13 1c0 0 1 0 1 0l1 0c0 0 0-1 1-1l1 0c1 0 2-1 2-3 0-1 0-3 0-5 0-2 0-4 0-6l0-5c1-7 1-15 0-22l0-4c0-5 0-11-1-17 0-1 0-3 0-5 0-1 0-3 0-4 0-2-1-7-7-8-10 0-20-1-30-2l-2 0c-3-1-8-1-12-1-5-1-7-1-8-1-1 0-1 0-2 0l-2 1c-1 1-1 2-1 3l0 74z m0-92c0 1 1 3 2 3l3 0 51 3c1 0 2 0 2-1 1 0 1-1 1-2l0-1c0 0 0-1 0-1 0 0 0 0-1-1 0 0 0-1 0-1l0-1c0-1-1-3-1-4-1-2-2-5-3-7l-1-3c-3-9-7-18-11-27-4-8-9-14-14-19-5-6-13-9-23-10l-3 0c-1 0-1 0-2 1 0 0 0 1 0 2l0 69z m-52-59c-1-1-1-2-2-2 0 0-1 1-1 1l-2 1c-27 6-64 40-78 79l-1 1c0 1 0 2 1 3 1 1 2 1 3 1l13-4c12-4 24-7 36-11 3-1 6-2 7-7 5-24 12-42 23-57l1-2c1-1 1-3 0-3z m40-10c0-1-1-1-1-2-1 0-2 0-2 0 0 0-1 0-1 0-1 0-14 4-20 11-15 19-23 41-29 61 0 0 0 0 0 1l0 1c0 1 0 1 1 2 0 1 1 1 2 1l1 0 29-2 17-1c2-1 3-2 3-3l0-69z m-21 165l18-2c2 0 3-1 3-2l0-75c0-1-1-2-1-2-1-1-2-1-2-1 0 0 0 0-1 0 0 0-14 2-21 2-8 1-17 2-26 3-4 0-6 4-6 7-2 18-3 35-3 47 0 5-1 22-1 22 0 1 1 2 1 2 1 1 1 1 2 1z m-98 32l1 1c12 31 42 59 76 71l3 1c1 0 2 0 3-1 0-1 0-2 0-3l-2-3c-18-25-25-53-31-79l0-1c0-1 0-2-1-2 0 0-1-1-1-1-1 0-1 0-1 0l-1 1c-5 1-11 3-17 4-8 2-18 4-27 8l0 1c-2 0-2 2-2 3z m-8-18l1 1c0 1 1 2 3 2l49-13c1 0 2-1 2-3l2-71c0-1 0-2-1-2-1-1-1-1-2-1 0 0 0 0 0 0l-1 0c-1 0-2 1-3 1l0 0c-2 0-4 1-7 1-16 4-29 9-39 17-6 4-9 8-9 15-1 19 1 37 5 53z m178 42c-8 16-14 31-21 46-8 17-17 35-24 52-14 31 3 65 36 71 28 5 56-16 59-44 0-9-1-18-5-26-14-32-29-64-44-97 0 0-1-1-1-2z m27 117c0 15-12 28-27 28-15 0-27-12-28-27 0-16 13-28 28-28 15 0 27 12 27 27z"/>
<glyph glyph-name="box" unicode="&#86;" d="M318 74l126 89 15-22-126-88z m-137 101l0-99 27 0 0 96z m145-125c-1 0-1 0-2 0l-262 26c-7 1-12 7-12 13l0 263c0 4 1 7 4 10 3 2 7 4 10 3l263-26c7 0 12-6 12-13l0-262c0-4-2-8-4-10-3-3-6-4-9-4z m-250 51l236-23 0 236-236 23z m377 326l-263 26c-3 1-7-1-10-3-3-3-4-6-4-10l0-21c3 2 7 3 11 3 0 0 0 0 1 0 3 2 7 4 11 4 2 0 3 0 5-1l234-23 0-236c0-7-3-10-10-14 1-2 1-5 1-7 1-2 0-3 0-5l21-2c1 0 1 0 2 0 3 0 6 1 8 4 3 2 5 6 5 10l0 262c0 7-5 13-12 13z m-397-64l125 88 16-22-126-88z m262-26l126 88 15-22-126-88z m-256-123l2 27 263-26-3-26z m146 37l0 91-27 0 0-88c9-1 18-2 27-3z"/>
<glyph glyph-name="community" unicode="&#48;" d="M50 175c-4 0-8 2-11 6-4 6-2 14 4 18l24 16 69 48 89-64c6-4 7-13 3-19-5-6-13-7-19-3l-74 53-53-37-24-16c-3-1-5-2-8-2z m130-10l-44 32-47-32-22-14 0-63 135 0 0 60z m120 10c-4 0-9 2-11 6-4 6-3 14 3 18l25 16 68 48 89-64c6-4 7-13 3-19-4-6-13-7-19-3l-73 53-54-37-24-16c-2-1-5-2-7-2z m129-10l-46 32-45-32-22-14 0-63 135 0 0 60z m-256 202c-4 0-9 2-11 5-4 7-3 15 4 19l24 16 68 48 89-65c6-4 7-12 3-18-4-6-12-7-18-3l-74 53-53-37-25-16c-2-2-5-2-7-2z m129-11l-46 32-45-31-22-15 0-62 135 0 0 60z"/>
<glyph glyph-name="grab-handle" unicode="&#88;" d="M280 318c0 10 9 19 20 19 10 0 19-9 19-19 0-11-9-20-19-20-11 0-20 9-20 20z m-46-20c-5 0-10 2-14 6-3 4-5 8-5 14 0 5 2 10 5 13 4 4 9 6 14 6 5 0 10-2 13-6 4-3 6-8 6-13 0-5-2-10-6-14-3-4-8-6-13-6z m46-42c0 10 9 19 20 19 10 0 19-9 19-19 0-11-9-20-19-20-11 0-20 9-20 20z m-46-20c-5 0-10 3-14 6-3 4-5 9-5 14 0 5 2 10 5 13 4 4 9 6 14 6 5 0 10-2 13-6 4-3 6-8 6-13 0-5-2-10-6-14-3-3-8-6-13-6z m46-42c0 10 9 19 20 19 10 0 19-9 19-19 0-11-9-19-19-19-11 0-20 8-20 19z m-46-19c-5 0-10 2-14 5-3 4-5 9-5 14 0 5 2 10 5 13 4 4 9 6 14 6 5 0 10-2 13-6 4-3 6-8 6-13 0-5-2-10-6-14-3-3-8-5-13-5z"/>
<glyph glyph-name="search" unicode="&#89;" d="M277 185c-48 0-88 40-88 88 0 49 40 89 88 89 49 0 89-40 89-89 0-48-40-88-89-88z m0 159c-38 0-70-32-70-71 0-38 32-70 70-70 39 0 71 32 71 70 0 39-32 71-71 71z m-112-205c-2 0-4 1-6 2-4 4-4 9 0 13l61 64c3 4 9 4 12 0 4-3 4-9 1-12l-61-64c-2-2-5-3-7-3z"/>
<glyph glyph-name="disclosure-collapse" unicode="&#90;" d="M264 198l-60 59c-4 5-4 12 0 17 5 4 13 4 17 0l43-43 43 44c4 4 12 4 17 0 4-5 4-13 0-17z"/>
<glyph glyph-name="script-upload" unicode="&#82;" d="M260 91l-117 0c-29 0-54 15-71 43-13 20-17 40-17 41l-3 16 157 0c7 0 13-5 13-12 0-7-6-12-13-12l-124 0c2-8 5-12 9-18 12-20 29-31 49-31l117 0c7 0 13-6 13-14 0-7-6-13-13-13z m117 7c-7 0-12 6-12 13 0 7 5 13 12 13 13 0 23 4 30 11 16 19 16 55 14 67l1 1 0 0 0 206-257 0 0-226c0-7-5-13-12-13-8 0-13 6-13 13l0 251 306 0 0-230c1-7 5-57-20-86-13-13-28-20-49-20z m6 253l-179 0c-8 0-14 6-14 14 0 8 6 14 14 14l179 0c8 0 14-6 14-14 0-8-6-14-14-14z m0-58l-179 0c-8 0-14 6-14 14 0 8 6 14 14 14l179 0c8 0 14-6 14-14 0-8-6-14-14-14z m0-58l-16 0c-8 0-14 6-14 14 0 8 6 14 14 14l16 0c8 0 14-6 14-14 0-8-6-14-14-14z m-110 0l-69 0c-8 0-14 6-14 14 0 8 6 14 14 14l69 0c8 0 14-6 14-14 0-8-6-14-14-14z m127-71l-80 83-81-83 49 0 0-136 62 22 0 114z"/>
<glyph glyph-name="code" unicode="&#87;" d="M92 242l74 73c6 6 15 6 21 0 6-5 6-15 0-21l-53-52 54-54c6-6 6-16 0-21-5-6-15-6-21 0z m347 0l-74 73c-5 6-15 6-21 0-5-5-5-15 0-21l53-52-54-54c-6-6-6-16 0-21 6-6 15-6 21 0z m-223-137c-1 0-2 0-4 1-5 2-8 8-6 14l98 254c3 6 9 8 15 6 5-2 8-8 6-14l-99-254c-1-4-5-7-10-7z"/>
<glyph glyph-name="avatar" unicode="&#60;" d="M256 88c-93 0-169 75-169 168 0 93 76 169 169 169 93 0 169-76 169-169 0-93-76-168-169-168z m0 316c-81 0-148-66-148-148 0-81 67-147 148-147 81 0 148 66 148 147 0 82-67 148-148 148z m97-90l-1 1c-3 3-7 4-10 4-1 0-61-9-86-9-1 0-1 0-2 0-25 0-87 10-87 10-5 0-10-2-13-6l-1-2c-2-3-2-7-1-10 1-4 3-6 6-8 12-5 49-20 60-22 2 0 5 0 6-7 1-8-3-46-7-65-5-17-13-40-13-41-2-6 1-13 7-15l8-3c3-1 6-1 9 1 3 1 5 4 6 7l21 65 20-67c1-3 3-6 6-7 2-1 4-1 5-1 2 0 3 0 5 0l7 3c5 2 8 8 7 14 0 0-6 24-11 44-3 12-4 30-5 45 0 9-1 16-2 22 0 1 0 4 5 5 0 0 1 0 2 0l55 22c4 2 6 5 7 9 1 4 0 8-3 11z m-68 37c0-16-13-29-29-29-16 0-29 13-29 29 0 16 13 29 29 29 16 0 29-13 29-29z"/>
<glyph glyph-name="arrows-h" unicode="&#58;" d="M512 256c0-5-2-9-5-13l-74-73c-3-4-7-5-12-5-5 0-10 1-13 5-4 4-6 8-6 13l0 36-292 0 0-36c0-5-2-9-6-13-3-4-8-5-13-5-5 0-9 1-12 5l-74 73c-3 4-5 8-5 13 0 5 2 9 5 13l74 73c3 4 7 5 12 5 5 0 10-1 13-5 4-4 6-8 6-13l0-36 292 0 0 36c0 5 2 9 6 13 3 4 8 5 13 5 5 0 9-1 12-5l74-73c3-4 5-8 5-13z"/>
<glyph glyph-name="arrows-v" unicode="&#59;" d="M347 421c0-5-1-10-5-13-4-4-8-6-13-6l-36 0 0-292 36 0c5 0 9-2 13-6 4-3 5-8 5-13 0-5-1-9-5-12l-73-74c-4-3-8-5-13-5-5 0-9 2-13 5l-73 74c-4 3-5 7-5 12 0 5 1 10 5 13 4 4 8 6 13 6l36 0 0 292-36 0c-5 0-9 2-13 6-4 3-5 8-5 13 0 5 1 9 5 12l73 74c4 3 8 5 13 5 5 0 9-2 13-5l73-74c4-3 5-7 5-12z"/>
<glyph glyph-name="arrows" unicode="&#96;" d="M512 256c0-5-2-9-5-13l-74-73c-3-4-7-5-12-5-5 0-10 1-13 5-4 4-6 8-6 13l0 36-109 0 0-109 36 0c5 0 9-2 13-6 4-3 5-8 5-13 0-5-1-9-5-12l-73-74c-4-3-8-5-13-5-5 0-9 2-13 5l-73 74c-4 3-5 7-5 12 0 5 1 10 5 13 4 4 8 6 13 6l36 0 0 109-109 0 0-36c0-5-2-9-6-13-3-4-8-5-13-5-5 0-9 1-12 5l-74 73c-3 4-5 8-5 13 0 5 2 9 5 13l74 73c3 4 7 5 12 5 5 0 10-1 13-5 4-4 6-8 6-13l0-36 109 0 0 109-36 0c-5 0-9 2-13 6-4 3-5 8-5 13 0 5 1 9 5 12l73 74c4 3 8 5 13 5 5 0 9-2 13-5l73-74c4-3 5-7 5-12 0-5-1-10-5-13-4-4-8-6-13-6l-36 0 0-109 109 0 0 36c0 5 2 9 6 13 3 4 8 5 13 5 5 0 9-1 12-5l74-73c3-4 5-8 5-13z"/>
<glyph glyph-name="compress" unicode="&#33;" d="M256 238l0-128c0-5-2-10-5-13-4-4-8-6-13-6-5 0-10 2-13 6l-41 41-95-95c-2-2-4-3-7-3-2 0-4 1-6 3l-33 33c-2 2-3 4-3 6 0 3 1 5 3 7l95 95-41 41c-4 3-6 8-6 13 0 5 2 9 6 13 3 3 8 5 13 5l128 0c5 0 9-2 13-5 3-4 5-8 5-13z m216 192c0-3-1-5-3-7l-95-95 41-41c4-3 6-8 6-13 0-5-2-9-6-13-3-3-8-5-13-5l-128 0c-5 0-9 2-13 5-3 4-5 8-5 13l0 128c0 5 2 10 5 13 4 4 8 6 13 6 5 0 10-2 13-6l41-41 95 95c2 2 4 3 7 3 2 0 4-1 6-3l33-33c2-2 3-4 3-6z"/>
<glyph glyph-name="expand" unicode="&#34;" d="M252 210c0-2-1-4-3-6l-94-95 41-41c3-4 5-8 5-13 0-5-2-9-5-13-4-4-8-5-13-5l-128 0c-5 0-9 1-13 5-4 4-5 8-5 13l0 128c0 5 1 9 5 13 4 3 8 5 13 5 5 0 9-2 13-5l41-41 95 94c2 2 4 3 6 3 3 0 5-1 7-3l32-32c2-2 3-4 3-7z m223 247l0-128c0-5-1-9-5-13-4-3-8-5-13-5-5 0-9 2-13 5l-41 41-95-94c-2-2-4-3-6-3-3 0-5 1-7 3l-32 32c-2 2-3 4-3 7 0 2 1 4 3 6l94 95-41 41c-3 4-5 8-5 13 0 5 2 9 5 13 4 4 8 5 13 5l128 0c5 0 9-1 13-5 4-4 5-8 5-13z"/>
<glyph glyph-name="placemark-1" unicode="&#35;" d="M475 213l0 176c-32-17-61-26-87-26-16 0-29 3-41 10-19 9-37 16-53 21-16 6-33 8-51 8-33 0-71-12-115-36l0-171c47 21 88 32 124 32 10 0 20-1 29-2 10-1 19-4 28-7 10-4 17-7 22-9 6-3 13-6 24-12l8-4c8-4 18-6 29-6 23 0 50 9 83 26z m-384 226c0-7-1-13-5-18-3-6-7-10-13-14l0-361c0-3-1-5-2-7-2-2-4-2-7-2l-18 0c-3 0-5 0-7 2-2 2-2 4-2 7l0 361c-6 4-10 8-14 14-3 5-5 11-5 18 0 10 4 19 11 26 7 7 16 10 26 10 10 0 19-3 26-10 7-7 10-16 10-26z m421-18l0-218c0-8-3-13-10-17-2-1-4-2-5-2-41-22-77-33-105-33-17 0-32 3-45 10l-8 4c-13 6-22 10-29 13-6 3-15 6-26 9-10 2-21 4-32 4-20 0-42-5-68-13-25-8-47-18-65-29-3-2-6-3-9-3-3 0-6 1-9 3-7 3-10 9-10 16l0 212c0 6 3 11 9 15 7 4 14 8 23 12 8 5 19 9 32 15 14 6 28 11 44 14 15 4 30 6 44 6 21 0 41-3 60-9 18-6 38-14 60-25 7-3 15-5 25-5 23 0 53 11 89 32 4 2 7 4 8 5 6 3 12 3 18-1 6-4 9-9 9-15z"/>
<glyph glyph-name="circle" unicode="&#36;" d="M366 238c0 35-13 65-38 90-25 25-55 38-90 38-36 0-66-13-91-38-25-25-37-55-37-90 0-36 12-66 37-91 25-25 55-37 91-37 35 0 65 12 90 37 25 25 38 55 38 91z m36 0c0-23-4-44-13-64-8-20-20-38-35-53-15-14-32-26-52-35-21-9-42-13-64-13-23 0-44 4-64 13-20 9-38 21-53 35-14 15-26 33-35 53-9 20-13 41-13 64 0 22 4 43 13 64 9 20 21 37 35 52 15 15 33 27 53 35 20 9 41 13 64 13 22 0 43-4 64-13 20-8 37-20 52-35 15-15 27-32 35-52 9-21 13-42 13-64z"/>
<glyph glyph-name="hand-pointer" unicode="&#57;" d="M183 475c-10 0-19-3-26-10-7-7-11-16-11-26l0-256-43 58c-8 10-18 15-30 15-10 0-19-4-26-11-7-7-10-16-10-26 0-8 2-15 7-22l110-146c7-10 17-14 29-14l205 0c4 0 8 1 11 3 4 3 6 6 7 10l26 105c5 19 7 37 7 56l0 62c0 8-3 14-8 20-5 6-12 9-20 9-7 0-14-3-19-8-5-6-8-12-8-20l-9 0 0 18c0 9-3 17-9 23-6 6-14 10-23 10-9 0-16-4-23-10-6-6-9-14-9-22l0-19-9 0 0 26c0 10-4 19-11 27-7 8-16 11-26 11-10 0-19-3-26-10-7-8-10-16-10-26l0-28-10 0 0 163c0 11-3 20-10 27-7 8-16 11-26 11z m0 37c20 0 38-7 52-22 14-15 21-32 21-53l0-63c4 1 7 1 9 1 19 0 35-7 50-20 9 4 18 6 28 6 21 0 39-8 52-25 6 2 11 2 16 2 18 0 33-6 46-19 12-13 18-28 18-46l0-62c0-22-2-44-8-64l-26-106c-3-12-9-22-19-29-10-8-21-12-34-12l-205 0c-12 0-22 3-33 8-10 5-19 12-26 21l-109 146c-10 13-15 28-15 44 0 20 7 38 21 52 14 14 32 22 52 22 13 0 25-4 37-10l0 156c0 20 7 37 21 52 14 14 32 21 52 21z m36-402l0 109-9 0 0-109z m74 0l0 109-10 0 0-109z m73 0l0 109-9 0 0-109z"/>
<glyph glyph-name="plus-square-o" unicode="&#37;" d="M384 283l0-18c0-3-1-5-3-6-1-2-3-3-6-3l-101 0 0-101c0-2-1-4-2-6-2-2-4-3-7-3l-18 0c-3 0-5 1-7 3-1 2-2 4-2 6l0 101-101 0c-3 0-5 1-6 3-2 1-3 3-3 6l0 18c0 3 1 5 3 7 1 2 3 3 6 3l101 0 0 100c0 3 1 5 2 7 2 1 4 2 7 2l18 0c3 0 5-1 7-2 1-2 2-4 2-7l0-100 101 0c3 0 5-1 6-3 2-2 3-4 3-7z m37-128l0 238c0 13-5 23-14 32-9 9-20 14-32 14l-238 0c-12 0-23-5-32-14-9-9-14-19-14-32l0-238c0-12 5-23 14-32 9-9 20-13 32-13l238 0c12 0 23 4 32 13 9 9 14 20 14 32z m36 238l0-238c0-22-8-42-24-58-16-16-35-24-58-24l-238 0c-23 0-42 8-58 24-16 16-24 36-24 58l0 238c0 23 8 42 24 58 16 16 35 24 58 24l238 0c23 0 42-8 58-24 16-16 24-35 24-58z"/>
<glyph glyph-name="square" unicode="&#39;" d="M375 439l-238 0c-12 0-23-5-32-14-9-9-14-19-14-32l0-238c0-12 5-23 14-32 9-9 20-13 32-13l238 0c12 0 23 4 32 13 9 9 14 20 14 32l0 238c0 13-5 23-14 32-9 9-20 14-32 14z m82-46l0-238c0-22-8-42-24-58-16-16-35-24-58-24l-238 0c-23 0-42 8-58 24-16 16-24 36-24 58l0 238c0 23 8 42 24 58 16 16 35 24 58 24l238 0c23 0 42-8 58-24 16-16 24-35 24-58z"/>
<glyph glyph-name="align-center" unicode="&#56;" d="M416 434l-320 0c-10 0-17-7-17-17l0-16c0-10 7-17 17-17l320 0c10 0 17 7 17 17l0 16c0 10-7 17-17 17z m-26-135l0 16c0 10-8 17-17 17l-234 0c-9 0-17-7-17-17l0-16c0-10 8-17 17-17l234 0c9 0 17 7 17 17z m8-171l-284 0c-10 0-18-7-18-17l0-16c0-10 8-17 18-17l284 0c10 0 18 7 18 17l0 0 0 16 0 0c0 10-8 17-18 17z m-25 85c0 10-8 17-18 17l-198 0c-10 0-18-7-18-17l0-16c0-10 8-17 18-17l198 0c10 0 18 7 18 17 0 0 0 0 0 0z"/>
<glyph glyph-name="align-justify" unicode="&#41;" d="M416 433l-320 0c-10 0-17-8-17-17l0-16c0-10 7-18 17-18l320 0c10 0 17 8 17 18l0 16c0 9-7 17-17 17z m0-101l-320 0c-10 0-17-8-17-17l0-16c0-10 7-18 17-18l320 0c10 0 17 8 17 18l0 16c0 9-7 17-17 17z m0-101l-320 0c-10 0-17-8-17-18l0-16c0-9 7-17 17-17l320 0c10 0 17 8 17 17l0 16c0 10-7 18-17 18z m0-101l-320 0c-10 0-17-8-17-18l0-16c0-9 7-17 17-17l320 0c10 0 17 8 17 17l0 16c0 10-7 18-17 18z"/>
<glyph glyph-name="align-left" unicode="&#42;" d="M416 434l-320 0c-10 0-17-7-17-17l0-16c0-10 7-17 17-17l320 0c10 0 17 7 17 17l0 16c0 10-7 17-17 17z m-320-152l234 0c9 0 17 7 17 17l0 16c0 10-8 18-17 18l-234 0c-10 0-17-8-17-18l0-16c0-10 7-17 17-17z m285-154l-285 0c-10 0-17-7-17-17l0-16c0-10 7-17 17-17l285 0c10 0 17 7 17 17l0 0 0 16 0 0c0 10-7 17-17 17z m-285 52l199 0c9 0 17 7 17 17 0 0 0 0 0 0l0 16c0 10-8 17-17 17l-199 0c-10 0-17-7-17-17l0-16c0-10 7-17 17-17z"/>
<glyph glyph-name="align-right" unicode="&#94;" d="M416 434l-320 0c-10 0-17-7-17-17l0-16c0-10 7-17 17-17l320 0c10 0 17 7 17 17l0 16c0 10-7 17-17 17z m0-102l-234 0c-9 0-17-7-17-17l0-16c0-10 8-17 17-17l234 0c10 0 17 7 17 17l0 16c0 10-7 17-17 17z m0-204l-285 0c-10 0-17-7-17-17l0-16c0-10 7-17 17-17l285 0c9 0 17 7 17 17l0 0 0 16 0 0c0 10-8 17-17 17z m17 85c0 10-7 17-17 17l-199 0c-9 0-17-7-17-17l0-16c0-10 8-17 17-17l199 0c10 0 17 7 17 17 0 0 0 0 0 0z"/>
<glyph glyph-name="bars" unicode="&#55;" d="M475 128l0-37c0-5-1-9-5-12-4-4-8-6-13-6l-402 0c-5 0-9 2-13 6-4 3-5 7-5 12l0 37c0 5 1 9 5 13 4 3 8 5 13 5l402 0c5 0 9-2 13-5 4-4 5-8 5-13z m0 146l0-36c0-5-1-10-5-13-4-4-8-6-13-6l-402 0c-5 0-9 2-13 6-4 3-5 8-5 13l0 36c0 5 1 10 5 13 4 4 8 6 13 6l402 0c5 0 9-2 13-6 4-3 5-8 5-13z m0 147l0-37c0-5-1-9-5-13-4-3-8-5-13-5l-402 0c-5 0-9 2-13 5-4 4-5 8-5 13l0 37c0 5 1 9 5 12 4 4 8 6 13 6l402 0c5 0 9-2 13-6 4-3 5-7 5-12z"/>
<glyph glyph-name="circle-slash" unicode="&#44;" d="M256 416c-88 0-160-72-160-160 0-88 72-160 160-160 88 0 160 72 160 160 0 88-72 160-160 160z m0-64c14 0 27-3 39-8l-127-127c-5 12-8 25-8 39 0 53 43 96 96 96z m0-192c-14 0-27 3-39 9l127 126c5-12 8-25 8-39 0-53-43-96-96-96z"/>
<glyph glyph-name="sync" unicode="&#40;" d="M392 275c6-41-7-84-39-115-47-47-119-52-173-17l38 36-138 19 19-134 42 40c76-55 183-50 251 18 40 39 58 91 56 142z m-233 77c47 46 119 52 173 17l-38-36 138-19-19 134-42-40c-76 55-183 50-251-18-40-39-58-91-56-142l56-11c-6 41 7 84 39 115z"/>
<glyph glyph-name="key" unicode="&#45;" d="M479 282c-1 2-1 4-3 6-2 2-5 3-7 3l-202 0c-13 51-60 89-115 89-65 0-119-54-119-119 0-66 54-119 119-119 59 0 107 43 117 99l45 0 0-65 0 0c0-5 5-9 10-9 0 0 0 0 0 0l0 0 31 0 0 0c5 0 10 4 10 9l0 0 0 65 26 0 0-100 0 0c0-5 4-9 9-9 0 0 0 0 0 0l32 0c0 0 0 0 0 0 5 0 9 4 10 9l0 0 0 100 27 0 0 0c2 0 5 0 7 2 2 2 3 5 3 7l0 0 0 32 0 0z m-327-90c-37 0-68 30-68 68 0 38 31 68 68 68 38 0 68-30 68-68 0-38-30-68-68-68z"/>
<glyph glyph-name="link" unicode="&#46;" d="M202 136c5 5 10 7 17 7 7 0 13-2 19-7 10-11 10-23 0-36 0 0-22-20-22-20-19-19-42-29-68-29-26 0-49 10-68 29-19 19-29 42-29 67 0 27 10 50 29 69 0 0 76 76 76 76 24 23 48 36 73 39 26 3 47-4 66-22 5-5 8-11 8-18 0-7-3-13-8-19-12-11-24-11-36 0-17 17-40 11-68-17 0 0-75-75-75-75-9-9-14-20-14-33 0-13 5-23 14-31 9-9 19-14 32-14 13 0 23 5 32 14 0 0 22 20 22 20m230 294c19-19 29-42 29-68 0-26-10-49-29-68 0 0-81-81-81-81-25-25-51-37-77-37-21 0-40 9-57 26-5 5-7 10-7 17 0 7 2 13 7 19 5 4 11 7 18 7 7 0 13-3 18-7 17-17 38-13 62 12 0 0 81 80 81 80 10 9 15 20 15 32 0 13-5 24-15 32-8 9-17 14-28 16-11 2-22-2-31-11 0 0-26-25-26-25-5-5-11-7-18-7-7 0-13 2-18 7-11 11-11 23 0 36 0 0 26 25 26 25 18 19 40 27 65 26 25-1 47-11 66-31"/>
<glyph glyph-name="location" unicode="&#47;" d="M256 512c-88 0-160-72-160-160 0-89 80-208 160-352 80 144 160 263 160 352 0 88-71 160-160 160z m0-224c-35 0-64 28-64 64 0 35 29 64 64 64 36 0 64-29 64-64 0-36-28-64-64-64z"/>
<glyph glyph-name="carat-r" unicode="&#51;" d="M304 249l-55-43 0 86z"/>
<glyph glyph-name="carat-l" unicode="&#52;" d="M225 250l55 43 0-87z"/>
<glyph glyph-name="folder-lg" unicode="&#62;" d="M203 385l38-51 185 0 0-206-333 0 0 257 110 0m0 30l-110 0c-17 0-30-14-30-30l0-257c0-17 13-30 30-30l333 0c17 0 30 13 30 30l0 206c0 16-13 30-30 30l-170 0-29 39c-6 7-15 12-24 12z"/>
<glyph glyph-name="folder-sm" unicode="&#63;" d="M226 324l20-27 98 0 0-109-176 0 0 136 58 0m0 24l-58 0c-13 0-24-11-24-24l0-136c0-13 11-24 24-24l176 0c13 0 24 11 24 24l0 109c0 13-11 24-24 24l-86 0-12 17c-5 6-12 10-20 10z"/>
<glyph glyph-name="level-up" unicode="&#49;" d="M331 274c-9 9-21 14-37 14l-68 0 32 32c5 4 5 12 0 17-2 2-5 3-8 3-3 0-6-1-9-3l-59-60 60-60c5-4 13-4 17 0 5 5 5 12 0 17l-31 31 66 0c9 0 16-2 19-7 6-6 5-15 5-16l0 0 0-75c0-6 5-12 12-12 6 0 11 6 11 12l0 73c0 5 1 21-10 34z"/>
<glyph glyph-name="info" unicode="&#91;" d="M267 218c-1-7 0-10 7-10l8 0-3-15c-7-3-13-4-18-4-11 0-22 6-19 24l8 56c-3 0-7 1-11 2l2 17 36 0z m15 94c-1-7-8-11-15-11-8 0-14 6-13 14 1 7 8 11 15 11 8 0 14-5 13-14z m65 29c-23 23-53 35-85 35-33 0-63-12-86-35-23-23-35-53-35-85 0-33 12-63 35-85 23-23 53-35 85-35 33 0 63 12 85 35 23 23 35 53 35 85 1 31-11 61-34 85z m18-85c0-28-11-55-31-73-19-20-45-31-73-31-28 0-54 11-73 31-20 19-31 45-31 73 0 27 11 53 31 73 19 20 45 31 73 31 28 0 54-11 73-31 20-20 31-46 31-73z"/>
<glyph glyph-name="question" unicode="&#93;" d="M360 346c1 0 3-1 3-3l0-181c0-2-2-3-3-3l-182 0c-1 0-3 1-3 3l0 181c0 2 2 3 3 3l182 0m0 17l-182 0c-11 0-20-9-20-20l0-181c0-11 9-20 20-20l182 0c11 0 20 9 20 20l0 181c0 11-9 20-20 20z m-133-55c20 7 29 9 47 9 28 0 40-10 40-34l0-5c0-17-6-24-17-28-8-3-16-5-25-8l0-17-20 0-3 30c12 3 22 7 30 9 8 3 11 7 11 13l0 4c0 13-4 16-18 16-10 0-16-1-24-4l-2-12-19 0z m21-110c0 9 5 13 14 13 10 0 15-4 15-13 0-9-5-13-15-13-9 0-14 4-14 13z"/>
<glyph glyph-name="alert" unicode="&#43;" d="M267 369l120-207-240 0 120 207m0 18c-6 0-12-3-16-9l-119-207c-4-6-4-12 0-18 3-6 9-9 15-9l240 0c6 0 12 3 15 9 4 6 4 12 0 18l-119 207c-4 6-10 9-16 9z m-15-195c0 9 5 13 15 13 9 0 14-4 14-13 0-9-5-13-14-13-10 0-15 4-15 13z m28 96l-6-67-15 0-6 67 0 21 27 0z"/>
<glyph glyph-name="home" unicode="&#95;" d="M265 376l133-122-49 0 0-112-169 0 0 112-49 0 134 122m0 20c-5 0-10-2-14-5l-134-122c-6-5-8-14-5-22 3-8 11-13 19-13l29 0 0-92c0-11 9-20 20-20l169 0c11 0 20 9 20 20l0 92 29 0c9 0 16 5 19 13 3 8 1 17-5 22l-134 122c-4 3-9 5-13 5z"/>
<glyph glyph-name="error" unicode="&#61;" d="M258 143c-29 0-58 11-80 33-21 22-33 50-33 80 0 30 12 59 33 80 44 44 116 44 160 0 22-21 34-50 34-80 0-30-12-58-34-80-22-22-51-33-80-33z m0 211c-25 0-50-9-69-29-18-18-29-43-29-69 0-26 11-50 29-69 38-38 100-38 139 0 18 19 28 43 28 69 0 26-10 51-28 69-20 20-45 29-70 29z m13-98l32 33c4 3 4 9 0 12-3 3-9 3-12 0l-33-33-32 33c-4 3-9 3-12 0-4-3-4-9 0-12l32-33-32-32c-4-4-4-9 0-13 3-3 8-3 12 0l32 33 33-33c3-3 9-3 12 0 4 4 4 9 0 13z"/>
<glyph glyph-name="settings" unicode="&#64;" d="M352 276c-3 0-6-1-8-2-1 0-1 0-1 0-3 0-6-1-8-2-2 9-6 17-11 25 3 1 6 2 8 4 0 0 0 0 0 0 3 1 5 3 7 5 8 8 8 20 0 28-7 7-20 7-27-1-2-2-4-4-5-7 0 0 0 0 0 0-2-2-4-5-4-8-8 5-16 9-25 11 1 2 2 5 2 8 0 0 0 1 0 1 1 2 2 5 2 8 0 11-9 19-20 19-10 0-19-8-19-19 0-3 1-6 2-8 0-1 0-1 0-1 0-3 1-6 2-8-9-2-18-6-25-11-1 3-2 6-5 8 0 0 0 0 0 0-1 3-2 5-4 7-8 8-20 8-28 0-7-7-7-20 0-27 2-2 5-4 7-5 1 0 1 0 1 0 2-2 4-4 7-4-5-8-8-16-10-25-3 1-6 2-9 2 0 0 0 0 0 0-2 1-5 2-8 2-11 0-20-9-20-20 0-10 9-19 20-19 3 0 6 0 8 2 0 0 0-1 0-1 4 0 6 1 9 3 2-9 5-18 10-25-2-1-5-2-7-5 0 0 0 0 0 0-3-1-6-2-8-4-7-8-7-20 0-28 4-4 9-5 14-5 5 0 10 1 14 5 2 2 4 5 4 7 1 1 1 1 1 1 2 2 3 4 4 7 8-5 16-8 25-10-1-3-2-6-2-9 0 0 0 0 0 0-1-2-2-5-2-8 0-11 9-20 20-20 10 0 19 9 19 20 0 3-1 6-2 8 0 0 0 0 0 0 0 3-1 6-2 9 9 2 17 5 25 10 1-3 2-5 4-7 1 0 1 0 1-1 1-2 2-5 4-7 4-4 9-5 14-5 5 0 10 1 14 5 7 8 7 20 0 28-2 2-5 3-7 4-1 0-1 0-1 0-2 3-4 4-7 5 5 7 8 16 10 25 3-1 6-2 9-2 0 0 0 0 0 0 2-1 5-2 8-2 11 0 19 9 19 19 0 0 0 0 0 0 0 0 0 1 0 1 0 10-9 19-19 19z m-90-61c-22 0-41 19-41 41 0 23 19 41 41 41 23 0 41-18 41-41 0-22-18-41-41-41z"/>
<glyph glyph-name="trash" unicode="&#123;" d="M201 302l0-165c0-3-1-5-2-6-2-2-4-3-7-3l-18 0c-3 0-5 1-7 3-2 1-2 3-2 6l0 165c0 2 0 5 2 6 2 2 4 3 7 3l18 0c3 0 5-1 7-3 1-1 2-4 2-6z m73 0l0-165c0-3-1-5-2-6-2-2-4-3-7-3l-18 0c-3 0-5 1-7 3-1 1-2 3-2 6l0 165c0 2 1 5 2 6 2 2 4 3 7 3l18 0c3 0 5-1 7-3 1-1 2-4 2-6z m73 0l0-165c0-3 0-5-2-6-2-2-4-3-7-3l-18 0c-3 0-5 1-7 3-1 1-2 3-2 6l0 165c0 2 1 5 2 6 2 2 4 3 7 3l18 0c3 0 5-1 7-3 2-1 2-4 2-6z m37-207l0 271-256 0 0-271c0-4 1-8 2-12 1-3 3-6 4-7 2-2 3-3 3-3l238 0c0 0 1 1 3 3 1 1 3 4 4 7 1 4 2 8 2 12z m-192 307l128 0-14 34c-1 1-3 2-5 3l-90 0c-2-1-4-2-5-3z m265-9l0-18c0-3-1-5-2-7-2-1-4-2-7-2l-27 0 0-271c0-16-5-30-14-41-9-12-20-17-32-17l-238 0c-12 0-23 5-32 16-9 11-14 25-14 41l0 272-27 0c-3 0-5 1-7 2-1 2-2 4-2 7l0 18c0 3 1 5 2 7 2 1 4 2 7 2l88 0 20 48c3 7 8 13 16 18 7 5 15 7 22 7l92 0c7 0 15-2 22-7 8-5 13-11 16-18l20-48 88 0c3 0 5-1 7-2 1-2 2-4 2-7z"/>
<glyph glyph-name="object-group" unicode="&#57344;" d="M549 402l-37 0 0-292 37 0 0-110-110 0 0 37-366 0 0-37-110 0 0 110 37 0 0 292-37 0 0 110 110 0 0-37 366 0 0 37 110 0z m-74 73l0-36 37 0 0 36z m-475 0l0-36 37 0 0 36z m37-438l0 36-37 0 0-36z m402 36l0 37 36 0 0 292-36 0 0 37-366 0 0-37-36 0 0-292 36 0 0-37z m73-36l0 36-37 0 0-36z m-183 292l110 0 0-219-256 0 0 73-110 0 0 219 256 0z m-219-110l183 0 0 147-183 0z m292-73l0 147-73 0 0-110-110 0 0-37z"/>
<glyph glyph-name="cm" unicode="&#125;" d="M202 207c-5 0-10 1-14 4-4 2-8 5-11 10-2 4-4 9-6 15-1 6-2 12-2 20 0 7 1 13 2 19 2 6 4 11 6 16 3 4 7 7 11 10 4 2 9 3 14 3 5 0 9-1 12-2 4-2 6-4 8-6l-8-11c-1 1-3 3-5 4-2 1-4 2-7 2-3 0-5-1-7-3-3-2-4-4-6-7-1-3-2-7-3-11-1-5-1-9-1-14 0-5 0-10 1-14 1-4 2-8 3-11 2-3 3-5 6-7 2-2 5-3 8-3 3 0 5 1 7 2 2 1 4 2 5 4l7-12c-5-5-11-8-20-8z m55 92c4 3 9 5 15 5 4 0 6 0 9-2 2-1 4-2 6-4 1-2 3-4 4-6 1-2 1-4 2-7l0 0c1 2 2 5 4 7 1 2 3 4 5 6 2 2 4 3 7 4 2 1 5 2 8 2 5 0 9-1 12-3 3-2 6-5 7-8 2-4 3-7 4-12 0-4 1-9 1-13l0-58-16 0 0 58c0 3 0 5 0 8-1 3-1 5-2 7-1 2-2 4-4 6-2 1-4 2-7 2-2 0-5-1-7-3-2-1-4-3-5-5-2-3-3-6-4-9-1-4-1-7-1-12l0-52-15 0 0 58c0 3 0 5-1 8 0 3-1 5-1 7-1 3-3 4-4 6-2 1-4 2-7 2-3 0-5-1-7-2-2-2-4-4-6-6-1-3-2-6-3-9-1-3-1-7-1-11l0-53-15 0 0 68c0 4-1 7-1 12 0 4 0 8 0 11l15 0c0-3 0-6 0-9 1-3 1-5 1-7l0 0c1 6 4 10 7 14z"/>
<glyph glyph-name="msvg" unicode="&#126;" d="M228 299c4 3 8 5 15 5 3 0 6 0 8-2 2-1 4-2 6-4 2-2 3-4 4-6 1-2 2-4 2-7l1 0c0 2 2 5 3 7 1 2 3 4 5 6 2 2 4 3 7 4 2 1 5 2 9 2 4 0 8-1 12-3 3-2 5-5 7-8 1-4 2-7 3-12 1-4 1-9 1-13l0-58-16 0 0 58c0 3 0 5 0 8 0 3-1 5-2 7-1 2-2 4-4 6-1 1-4 2-7 2-2 0-5-1-7-3-2-1-4-3-5-5-2-3-3-6-4-9 0-4-1-7-1-12l0-52-15 0 0 58c0 3 0 5 0 8-1 3-1 5-2 7-1 3-2 4-4 6-2 1-4 2-7 2-2 0-5-1-7-2-2-2-4-4-5-6-2-3-3-6-4-9-1-3-1-7-1-11l0-53-15 0 0 68c0 4 0 7 0 12 0 4-1 8-1 11l15 0c0-3 0-6 1-9 0-3 0-5 0-7l0 0c1 6 4 10 8 14z"/>
<glyph glyph-name="deg" unicode="&#92;" d="M199 210l0 14-1 0c-1-4-4-9-7-12-3-3-8-5-14-5-5 0-9 1-12 4-4 2-7 6-9 10-3 4-5 9-6 15-1 6-2 13-2 20 0 7 1 13 2 19 1 6 3 11 5 16 3 4 6 7 10 10 3 2 7 3 12 3 5 0 10-1 13-4 4-3 7-7 8-12l0 0 0 59 16 0 0-137z m0 45c0 5-1 10-1 14-1 5-2 8-4 11-1 3-3 6-5 8-2 2-5 2-8 2-3 0-5 0-8-2-2-2-3-5-5-8-1-3-2-6-3-11-1-4-1-9-1-14 0-4 0-9 1-13 1-4 2-8 3-11 2-3 3-6 5-8 3-1 5-2 8-2 3 0 6 1 8 2 2 2 4 5 5 8 2 3 3 7 4 11 0 4 1 9 1 13z m49-3c0-5 1-9 1-13 1-3 2-7 4-9 1-3 3-5 5-7 3-2 5-2 8-2 5 0 8 1 10 3 3 3 5 6 6 9l12-6c-3-6-6-11-11-15-4-3-10-5-17-5-10 0-19 4-24 13-6 8-9 20-9 35 0 8 0 14 2 20 2 6 4 11 7 16 3 4 6 7 10 10 4 2 9 3 14 3 5 0 10-1 14-3 4-3 7-6 9-10 3-4 5-9 6-14 1-6 1-12 1-18l0-7-48 0z m33 12c0 8-2 14-4 19-3 5-6 7-12 7-3 0-6 0-8-2-2-2-3-4-5-7-1-3-2-6-3-9 0-3-1-6-1-8z m79 37l15 0 0-91c0-6-1-12-2-17-1-5-3-10-6-14-2-4-6-7-10-9-5-2-10-3-16-3-7 0-12 1-18 4-5 2-10 6-13 10l9 11c3-3 6-6 10-8 3-2 8-3 12-3 4 0 7 0 9 2 3 1 5 3 6 6 1 3 2 6 3 9 1 4 1 8 1 12l0 14-1 0c-1-5-3-9-7-12-3-3-7-5-14-5-4 0-8 1-12 4-4 2-7 6-9 10-3 4-4 9-6 15-1 6-2 13-2 20 0 7 1 13 2 19 1 6 3 11 6 15 2 5 5 8 9 10 3 3 8 4 12 4 6 0 10-2 14-5 3-3 6-6 7-11l1 0 0 13z m0-45c0 4 0 9-1 13-1 5-2 8-3 11-2 4-4 6-6 8-2 2-5 3-8 3-3 0-5-1-7-3-2-2-4-4-5-8-2-3-3-6-4-11 0-4-1-9-1-13 0-5 1-10 1-14 1-4 2-8 4-11 1-3 3-6 5-8 2-1 4-2 7-2 3 0 6 1 8 2 2 2 4 5 6 8 1 3 2 7 3 11 1 4 1 9 1 14z"/>
<glyph glyph-name="px" unicode="&#124;" d="M206 301l0-14 1 0c1 5 3 9 7 12 3 3 8 5 14 5 4 0 9-1 12-4 4-2 7-6 9-10 3-4 5-9 6-15 1-6 2-13 2-20 0-7-1-13-2-19-1-6-3-11-6-15-2-5-5-8-9-10-3-3-7-4-12-4-5 0-10 1-14 5-3 3-6 7-7 11l0 0 0-56-16 0 0 134z m0-45c0-5 1-10 1-14 1-4 2-8 4-11 1-3 3-6 5-8 2-1 5-2 8-2 3 0 5 1 7 2 3 2 4 5 6 8 1 3 2 7 3 11 0 4 1 9 1 14 0 4-1 9-1 13-1 5-2 8-3 11-2 3-3 6-6 8-2 2-4 2-7 2-3 0-6 0-8-2-2-2-4-5-5-8-2-3-3-6-4-11 0-4-1-9-1-13z m86 2l-23 43 17 0 15-32 13 32 17 0-22-43 24-48-17 0-16 36-16-36-17 0z"/>
<glyph glyph-name="m-sq" unicode="&#57345;" d="M204 299c4 3 8 5 15 5 3 0 6 0 8-2 2-1 4-2 6-4 2-2 3-4 4-6 1-2 2-4 2-7l1 0c0 2 2 5 3 7 1 2 3 4 5 6 2 2 4 3 7 4 2 1 5 2 9 2 4 0 8-1 12-3 3-2 5-5 7-8 1-4 2-7 3-12 1-4 1-9 1-13l0-58-16 0 0 58c0 3 0 5 0 8 0 3-1 5-2 7-1 2-2 4-4 6-1 1-4 2-7 2-2 0-5-1-7-3-2-1-4-3-5-5-2-3-3-6-4-9 0-4-1-7-1-12l0-52-15 0 0 58c0 3 0 5 0 8-1 3-1 5-2 7-1 3-2 4-4 6-2 1-4 2-7 2-2 0-5-1-7-2-2-2-4-4-5-6-2-3-3-6-4-9-1-3-1-7-1-11l0-53-15 0 0 68c0 4 0 7 0 12 0 4-1 8-1 11l15 0c0-3 0-6 1-9 0-3 0-5 0-7l0 0c1 6 4 10 8 14z m129 44c0-2-1-4-1-5-1-2-1-4-2-6-1-1-2-3-3-5-2-2-3-3-4-5l-14-20 23 0 0-7-32 0 0 8 18 24c2 3 3 6 4 8 2 3 2 5 2 8 0 3-1 5-2 7-1 3-3 4-6 4-2 0-4-1-6-3-2-2-3-4-3-7l-8 1c1 5 3 9 6 12 3 2 7 4 12 4 2 0 4-1 6-2 2-1 4-2 5-3 2-2 3-4 3-6 1-2 2-4 2-7z"/>
<glyph glyph-name="m-cubed" unicode="&#57346;" d="M204 299c4 3 8 5 15 5 3 0 6 0 8-2 2-1 4-2 6-4 2-2 3-4 4-6 1-2 2-4 2-7l1 0c0 2 2 5 3 7 1 2 3 4 5 6 2 2 4 3 7 4 2 1 5 2 9 2 4 0 8-1 12-3 3-2 5-5 7-8 1-4 2-7 3-12 1-4 1-9 1-13l0-58-16 0 0 58c0 3 0 5 0 8 0 3-1 5-2 7-1 2-2 4-4 6-1 1-4 2-7 2-2 0-5-1-7-3-2-1-4-3-5-5-2-3-3-6-4-9 0-4-1-7-1-12l0-52-15 0 0 58c0 3 0 5 0 8-1 3-1 5-2 7-1 3-2 4-4 6-2 1-4 2-7 2-2 0-5-1-7-2-2-2-4-4-5-6-2-3-3-6-4-9-1-3-1-7-1-11l0-53-15 0 0 68c0 4 0 7 0 12 0 4-1 8-1 11l15 0c0-3 0-6 1-9 0-3 0-5 0-7l0 0c1 6 4 10 8 14z m130 14c0-3-1-5-2-7 0-3-2-5-3-7-2-1-3-3-6-4-2-1-4-1-7-1-5 0-8 1-11 3-4 2-6 6-7 10l7 2c1-2 2-4 4-6 2-1 4-2 7-2 1 0 3 0 4 1 1 1 2 2 3 3 1 1 2 2 2 4 1 1 1 3 1 4 0 4-1 7-3 9-3 2-5 3-9 3l-2 0 0 7 2 0c3 0 6 1 8 3 1 2 2 5 2 8 0 2 0 3 0 4 0 1-1 2-2 3 0 1-1 2-2 3-1 1-3 1-4 1-2 0-4-1-5-2-2-1-3-2-4-4l-7 2c1 2 2 3 3 5 1 1 3 2 4 3 1 1 3 2 4 2 2 1 4 1 5 1 3 0 5-1 7-1 2-1 3-2 5-4 1-1 2-3 3-5 1-2 1-4 1-7 0-2 0-3 0-5-1-2-1-3-2-4-1-2-2-3-3-4-1-1-3-1-4-2l0 0c3-1 6-3 8-6 2-2 3-6 3-10z"/>
<glyph glyph-name="acceleration" unicode="&#57347;" d="M207 350c3 2 8 3 14 3 3 0 5 0 8-1 2 0 4-1 5-2 2-1 3-2 4-4 1-1 2-3 2-4l0 0c1 1 2 3 3 4 2 1 3 3 5 4 2 1 4 2 7 2 2 1 5 1 8 1 5 0 8 0 11-1 3-2 5-3 7-5 1-2 2-5 3-7 1-3 1-6 1-8l0-35-15 0 0 35c0 1 0 3 0 4 0 2-1 4-2 5-1 1-2 2-3 3-2 1-4 1-7 1-2 0-4 0-6-1-2-1-4-2-6-4-1-1-2-3-3-5-1-2-1-4-1-7l0-31-14 0 0 35c0 1-1 3-1 4 0 2-1 3-1 5-1 1-2 2-4 3-2 1-4 1-7 1-2 0-4 0-6-1-2-1-4-2-5-4-2-1-3-3-3-5-1-2-2-4-2-7l0-32-14 0 0 41c0 3 0 5 0 8 0 2 0 4-1 6l14 0c0-1 1-3 1-5 0-2 0-3 0-4l0 0c2 3 4 6 8 8z m112-92c0-2 0-4-1-5 0-1-1-3-2-4 0-2-1-3-2-4-1-2-2-3-3-5l-11-15 18 0 0-6-25 0 0 6 14 20c2 2 3 4 4 6 1 2 1 4 1 6 0 3 0 5-1 6-1 2-3 3-5 3-2 0-4-1-5-2-1-2-2-3-2-6l-7 1c1 4 3 7 5 9 2 2 5 3 9 3 2 0 4 0 6-1 1 0 3-1 4-3 1-1 2-2 2-4 1-2 1-3 1-5z m-112-18c-1 1-2 2-3 3-1 1-3 1-5 1-1 0-3 0-4-2-1-1-2-3-2-5 0-2 1-3 2-4 1-1 3-2 6-3 1-1 2-1 3-2 2-1 3-2 4-3 1-1 2-2 2-4 1-1 1-3 1-5 0-2 0-4-1-6-1-2-2-3-3-5-1-1-3-2-4-2-2-1-4-1-6-1-3 0-5 0-7 1-3 1-4 3-6 5l5 5c1-2 2-3 3-4 2 0 3-1 5-1 2 0 4 1 5 2 1 2 2 3 2 6 0 1 0 2-1 3 0 1-1 2-2 2 0 1-1 1-2 2-1 0-2 1-3 1-2 1-3 1-4 2-1 0-2 1-3 2-1 1-1 2-2 4 0 1-1 3-1 5 0 2 1 4 1 5 1 2 2 3 3 5 1 1 3 2 4 2 2 1 4 1 6 1 2 0 5 0 7-1 2-1 3-2 5-4z m18-16c0-2 0-4 0-6 1-2 1-4 2-5 1-1 2-3 3-3 1-1 3-2 4-2 2 0 4 1 5 2 1 2 2 3 3 5l6-3c-2-3-3-6-6-8-2-1-5-2-8-2-5 0-10 2-13 6-3 4-4 10-4 18 0 4 0 7 1 10 1 3 2 6 4 8 1 2 3 3 5 5 2 1 4 1 7 1 2 0 5 0 7-1 2-2 3-3 5-5 1-2 2-5 2-7 1-3 1-6 1-9l0-4-24 0z m16 6c0 4 0 7-2 10-1 2-3 4-6 4-1 0-3-1-4-2-1-1-2-2-2-3-1-2-1-3-2-5 0-1 0-2 0-4l16 0z m31-28c-3 0-5 0-7 1-2 2-4 3-5 5-2 3-3 5-3 8-1 3-1 6-1 10 0 4 0 7 1 10 0 3 1 5 3 8 1 2 3 3 5 5 2 1 5 1 7 1 3 0 5 0 6-1 2-1 3-1 4-2l-4-6c0 1-1 1-2 2-1 0-2 1-4 1-1 0-2-1-3-2-1-1-2-2-3-3-1-2-1-4-2-6 0-2 0-4 0-7 0-2 0-5 0-7 1-2 1-4 2-5 1-2 2-3 3-4 1-1 2-1 4-1 1 0 3 0 4 1 1 0 1 1 2 2l4-6c-3-3-6-4-11-4z m17 74l-107 0c-2 0-4 2-4 4 0 2 2 4 4 4l107 0c2 0 4-2 4-4 0-2-2-4-4-4z"/>
<glyph glyph-name="particles" unicode="&#57348;" d="M332 229c0 12 10 23 23 23 13 0 23-11 23-23 0-13-10-24-23-24-13 0-23 11-23 24z m-54-68c0 13 10 23 23 23 13 0 23-10 23-23 0-13-10-23-23-23-13 0-23 10-23 23z m-62-18c0 13 11 23 24 23 12 0 23-10 23-23 0-13-11-23-23-23-13 0-24 10-24 23z m-46 60c0 13 10 23 23 23 13 0 23-10 23-23 0-13-10-23-23-23-13 0-23 10-23 23z m81-138c-5 3-8 9-9 15-1 6 1 12 4 17 4 5 9 8 15 9 6 1 13 0 18-4 5-4 8-9 9-15 1-6-1-13-4-18-4-4-9-8-16-9-6-1-12 1-17 5z m-183 201c0 13 10 23 23 23 13 0 23-10 23-23 0-13-10-23-23-23-13 0-23 10-23 23z m111 46c0 13 10 24 23 24 13 0 23-11 23-24 0-12-10-23-23-23-13 0-23 11-23 23z m71-94c0 13 11 23 24 23 12 0 23-10 23-23 0-13-11-23-23-23-13 0-24 10-24 23z m163 52c0 13 10 23 23 23 13 0 23-10 23-23 0-13-10-23-23-23-12 0-23 10-23 23z m-111 38c0 13 11 23 23 23 13 0 24-10 24-23 0-13-11-23-24-23-12 0-23 10-23 23z m-170 90c0 13 11 24 24 24 12 0 23-11 23-24 0-12-11-23-23-23-13 0-24 11-24 23z m235-23c0 13 10 23 23 23 13 0 23-10 23-23 0-13-10-23-23-23-13 0-23 10-23 23z"/>
<glyph glyph-name="voxels" unicode="&#57349;" d="M434 379l-85 49c-4 2-10 2-14 0l-77-45-79 46c-4 2-10 2-14 0l-85-49c-4-3-7-7-7-12l0-98c0-5 3-10 7-13l78-45 0-89c0-5 3-10 7-12l85-49c2-2 5-2 7-2 2 0 5 0 7 2l85 49c4 2 7 7 7 12l0 88 78 45c5 3 7 7 7 12l0 99c0 5-2 9-7 12z m-21-88l-59 34 0 68 59-35z m-69-55l-73 42 0 80 59 35 0-68-29-17c-6-3-8-11-4-16 2-4 6-6 10-6 2 0 4 0 6 1l29 17 60-34z m-75-57l0 66 59-34 0-66z m-26 113l-59 34 0 68 59-34z m-142 68l59 34 0-68-31-18c-6-3-8-11-5-17 2-3 7-6 11-6 2 0 4 1 6 2l31 18 59-34-60-34-70 41z m156-270l-71 41 0 81 59 34 0-67-28-16c-6-3-8-11-5-17 3-3 7-6 11-6 2 0 4 1 6 2l28 16 59-34z"/>
<glyph glyph-name="lock" unicode="&#57350;" d="M389 233l0 62c0 68-55 124-123 124-69 0-124-56-124-124l0-62c-24-4-44-26-44-52l0-74c0-29 24-52 52-52l230 0c29 0 53 23 53 52l0 74c0 26-18 48-44 52z m-123 129c37 0 67-30 67-67l0-61-135 0 0 61c0 37 31 67 68 67z"/>
<glyph glyph-name="visible" unicode="&#57351;" d="M258 116c-55 0-106 17-147 51-31 25-47 51-47 52-4 7-4 16 1 23 2 4 66 98 195 96 133-3 192-93 195-97 4-6 4-15 0-22 0-1-15-27-46-53-29-23-79-50-151-50 0 0 0 0 0 0z m-148 113c7-7 17-18 30-29 34-27 73-40 118-40 0 0 0 0 0 0 47 0 88 13 122 40 13 10 23 21 29 29-7 7-16 16-30 26-34 25-74 38-119 38-81 2-130-42-150-64z m-27 1z m227-4c0-25-21-46-47-46-26 0-47 21-47 46 0 26 21 47 47 47 26 0 47-21 47-47z"/>
<glyph glyph-name="model" unicode="&#57352;" d="M494 395c-2 5-8 8-13 7l-90-16 45 72c3 5 2 11-1 15-4 4-10 5-15 3l-213-98c-15 5-72 27-111 43 0 0-1 0-1 0 0 0 0 0 0 0 0 0-1 0-1 1 0 0-1 0-1 0 0 0-1 0-1 0 0 0 0 0 0 0-1 0-1 0-2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0-1 0-1 0 0 0 0 0 0-1-1 0-1 0-2 0 0 0 0 0 0 0 0 0-1 0-1 0 0 0 0-1 0-1-1 0-2 0-3-1 0 0 0 0 0 0 0-1-1-1-1-1 0 0 0 0 0 0 0 0 0-1 0-1-1 0-1 0-1 0 0 0 0 0 0-1 0 0 0 0-1-1l-27-52-33-40c-3-4-3-10 0-15 2-3 6-5 10-5 1 0 2 0 4 1l50 17 40 2-26-51c-3-4-2-9 1-13 1-1 26-30 52-58 15-17 28-30 38-40 6-6 11-11 15-14l-16-61-46-18c-6-3-9-10-6-16 2-5 6-8 11-8 1 0 3 1 4 1l45 18 17-15c2-3 5-4 8-4 4 0 7 2 9 5 5 5 4 12-1 17l-17 15 16 61 76-90c2-2 6-4 9-4 1 0 2 0 3 0l85 23c5 2 8 6 9 11 0 5-2 9-7 12l-136 72 45 91 178 123c5 3 6 9 4 15z m-200-117l-122 55 41 21 181 83z m-148 73l-24 33c16-6 39-15 54-21z m-59-6l-9 13 15 29 27-38 2-2z m36-77l23 44c18-45 35-91 47-121-19 20-45 49-70 77z m194-194l-57 68 105-55z m-94 101c-5 14-42 104-30 77 0-2-21 49-28 66l121-59-48-95z m108 120l43 63 70 16z"/>
<glyph glyph-name="forward" unicode="&#68;" d="M330 278l-95 70c-5 4-12 5-18 2-5-3-9-9-9-16l0-150c0-7 4-13 10-16 2-1 5-2 7-2 4 0 8 2 11 5l95 79c4 4 6 9 6 14-1 5-3 10-7 14"/>
<glyph glyph-name="avatar-2" unicode="&#57353;" d="M256 88c-93 0-169 75-169 168 0 93 76 169 169 169 93 0 169-76 169-169 0-93-76-168-169-168z m0 316c-81 0-148-66-148-148 0-81 67-147 148-147 81 0 148 66 148 147 0 82-67 148-148 148z m97-90l-1 1c-3 3-7 4-10 4-1 0-61-9-86-9-1 0-1 0-2 0-25 0-87 10-87 10-5 0-10-2-13-6l-1-2c-2-3-2-7-1-10 1-4 3-6 6-8 12-5 49-20 60-22 2 0 5 0 6-7 1-8-3-46-7-65-5-17-13-40-13-41-2-6 1-13 7-15l8-3c3-1 6-1 9 1 3 1 5 4 6 7l21 65 20-67c1-3 3-6 6-7 2-1 4-1 5-1 2 0 3 0 5 0l7 3c5 2 8 8 7 14 0 0-6 24-11 44-3 12-4 30-5 45 0 9-1 16-2 22 0 1 0 4 5 5 0 0 1 0 2 0l55 22c4 2 6 5 7 9 1 4 0 8-3 11z m-68 37c0-16-13-29-29-29-16 0-29 13-29 29 0 16 13 29 29 29 16 0 29-13 29-29z"/>
<glyph glyph-name="arrow-dn" unicode="&#53;" d="M258 219l-43 55 86 0z"/>
<glyph glyph-name="arrow-up" unicode="&#54;" d="M258 283l43-55-86 0z"/>
<glyph glyph-name="time" unicode="&#57354;" d="M256 390c-73 0-132-59-132-132 0-73 59-132 132-132 73 0 132 59 132 132 0 73-59 132-132 132z m60-162l1 0-64 0c-2 0-4 0-6 1-8 2-15 10-15 19l0 0 2 92c0 11 9 20 19 20 11 0 20-9 20-20l-1-72 44 0c11 0 20-9 20-20 0-10-9-20-20-20z"/>
<glyph glyph-name="transparency" unicode="&#57355;" d="M349 349c1 0 3-1 3-3l0-181c0-2-2-3-3-3l-182 0c-1 0-3 1-3 3l0 181c0 2 2 3 3 3l182 0m0 17l-182 0c-11 0-20-9-20-20l0-181c0-11 9-20 20-20l182 0c11 0 20 9 20 20l0 181c0 11-9 20-20 20z m-187-108l96 0 0-96-96 0z m94 94l96 0 0-95-96 0z"/>
<glyph glyph-name="unmuted" unicode="&#71;" d="M298 255c-1-8-2-17-3-25-2-7-4-15-6-22-2-6-1-11 3-14 7-6 16-3 19 6 8 23 12 47 9 71-1 14-4 27-9 40-3 7-12 10-18 5-5-3-7-8-4-14 5-15 8-31 9-47m-35 81c0 12-5 20-16 24-7 3-19 2-27-7-11-12-22-24-34-37-1-1-3-1-5-2-4 0-7 0-11 0-3 0-5 0-8 0-15 0-26-10-26-23 0-28 0-42 0-71 0-11 10-22 21-23 4 0 9 0 14 0 3-1 7-1 10-1 2 0 4-1 5-2 10-11 22-23 33-36 6-6 12-9 20-9 3 0 5 1 8 2 11 3 16 12 16 24 0 22 0 39 0 59l0 43c0 19 0 59 0 59m-26-3l0-155-1 1c-3 3-5 5-7 7l-8 9c-8 9-16 17-23 25-2 2-4 2-6 2-4 1-9 1-13 1-2 0-4 0-7 0l-9 0 0 65 2 0c3 0 5 0 8 0 6 0 12 0 18 0 0 0 0 0 0 0 3 0 5 1 8 4 10 10 20 21 30 32z m140-78c0 3-1 6-1 10-1 7-1 14-2 22-3 15-7 31-14 47-2 4-5 7-9 8-4 1-8 0-11-3-5-4-6-10-4-16 9-22 13-42 14-64 1-24-4-49-14-72-1-4-2-8 0-11 1-3 4-6 7-7 2-1 4-1 5-1 6 0 10 3 13 9 10 25 15 51 16 78z"/>
<glyph glyph-name="user" unicode="&#57356;" d="M257 406c-83 0-151-68-151-151 0-83 68-150 151-150 83 0 150 67 150 150 0 83-67 151-150 151z m0-282c-73 0-132 59-132 131 0 73 59 132 132 132 73 0 132-59 132-132 0-72-59-131-132-131z m45 179c0-24-19-43-42-43-24 0-43 19-43 43 0 23 19 42 43 42 23 0 42-19 42-42z m-10-71l-62 0c-25 0-53-12-53-37l0-18c26-23 50-32 77-32 32 0 69 14 85 33l0 17c0 25-22 37-47 37z"/>
<glyph glyph-name="edit-pencil" unicode="&#57357;" d="M341 403c9 9 18 19 28 28 4 4 9 3 12-1 17-16 33-32 50-49 3-4 4-8 0-12-10-10-20-19-29-29-20 21-41 42-61 63z m-25-23c-1-2-2-4-3-5-9-9-17-17-26-26-49-49-153-152-175-175-5-5-9-11-10-19-3-22-6-43-10-66 3 1 4 1 6 1 20 4 41 8 62 13 4 1 8 3 11 6 21 21 109 108 163 162 13 14 27 28 41 42 2 1 3 3 4 3-21 21-41 42-63 64z m-158-231c-6 6-12 12-17 17 21 21 137 137 177 177 5-5 11-11 16-17-7-7-14-14-21-22-50-50-138-137-155-155z"/>
<glyph glyph-name="muted" unicode="&#72;" d="M377 274l-57-57c-5-5-13-5-18 0-5 5-5 13 0 18l57 57c5 5 13 5 18 0 5-5 5-13 0-18m-18-57l-57 57c-5 5-5 13 0 18 5 5 13 5 18 0l57-57c5-5 5-13 0-18-5-5-13-5-18 0m-95 120c0 12-6 21-17 25-8 3-20 3-29-7-11-12-23-25-35-37-1-2-3-2-4-3-4 0-8 0-12 0-3 0-6 0-8 0-15 0-27-11-27-24 0-29 0-42 0-73 0-11 10-22 21-23 5-1 10-1 15-1 4 0 7 0 11 0 1 0 3-1 4-2 11-12 23-24 35-37 5-7 12-10 20-10 3 0 6 1 9 2 11 4 17 13 17 25 0 22 0 40 0 60l0 45c0 20 0 60 0 60m-28-3l0-159-1 1c-3 3-5 5-7 8l-9 9c-8 9-16 17-24 26-1 1-3 2-5 2-5 0-9 0-14 0-2 0-5 0-7 0l-10 0 0 67 3 0c3 0 5 0 8 0 6 0 12 0 19 0 0 0 0 0 0 0 3 0 5 1 8 4 10 11 20 22 31 34z"/>
<glyph glyph-name="vol-0" unicode="&#57358;" d="M109 352c-2-2-5-4-8-4-7 0-13 0-18 0-2 0-4 0-7 0-2 0-4 0-6 0-23 0-41-16-41-37 0-44 0-65 0-113 1-17 15-33 32-35 8-1 16-1 23-1l1 0c5 0 11 0 16 0 3 0 6-2 8-4 15-15 35-37 54-58 8-9 18-14 30-14 4 0 8 1 12 2 16 6 23 19 23 39l0 162c0 31 2 94 2 95 0 19-9 31-25 37-11 5-29 4-42-10-20-23-37-42-54-59z m66 14l15 18 0-259-4 7c-2 2-4 4-6 6-1 2-3 4-5 6l-13 14c-3 3-6 6-9 10-9 10-18 20-27 30-2 2-6 3-8 3-6 0-13 0-21 0l-29 0 0 106 7 0c3 0 5 0 8 0 1 0 2 0 4 0 5 0 11 0 16 0 4 0 9 0 13 0l1 0c4 0 7 2 11 6 7 8 14 16 21 23 8 10 17 20 26 30z"/>
<glyph glyph-name="vol-1" unicode="&#57359;" d="M109 352c-2-2-5-4-8-4-7 0-13 0-18 0-2 0-4 0-7 0-2 0-4 0-6 0-23 0-41-16-41-37 0-44 0-65 0-113 1-17 15-33 32-35 8-1 16-1 23-1l1 0c5 0 11 0 16 0 3 0 6-2 8-4 15-15 35-37 54-58 8-9 18-14 30-14 4 0 8 1 12 2 16 6 23 19 23 39l0 162c0 31 2 94 2 95 0 19-9 31-25 37-11 5-29 4-42-10-20-23-37-42-54-59z m66 14l15 18 0-259-4 7c-2 2-4 4-6 6-1 2-3 4-5 6l-13 14c-3 3-6 6-9 10-9 10-18 20-27 30-2 2-6 3-8 3-6 0-13 0-21 0l-29 0 0 106 7 0c3 0 5 0 8 0 1 0 2 0 4 0 5 0 11 0 16 0 4 0 9 0 13 0l1 0c4 0 7 2 11 6 7 8 14 16 21 23 8 10 17 20 26 30z m89-110c0 32-15 58-34 58-19 0-34-26-34-58 0-32 15-58 34-58 19 0 34 26 34 58z"/>
<glyph glyph-name="vol-2" unicode="&#57360;" d="M304 254c-2-13-3-26-5-39-2-12-6-24-10-36-3-10-1-17 5-22 12-9 26-5 31 9 14 37 19 75 15 115-3 21-8 43-16 63-4 12-18 16-28 9-8-5-10-13-7-24 9-24 14-49 15-75m-195 98c-2-2-5-4-8-4-7 0-13 0-18 0-2 0-4 0-7 0-2 0-4 0-6 0-23 0-41-16-41-37 0-44 0-65 0-113 1-17 15-33 32-35 8-1 16-1 23-1l1 0c5 0 11 0 16 0 3 0 6-2 8-4 15-15 35-37 54-58 8-9 18-14 30-14 4 0 8 1 12 2 16 6 23 19 23 39l0 162c0 31 2 94 2 95 0 19-9 31-25 37-11 5-29 4-42-10-20-23-37-42-54-59z m66 14l15 18 0-259-4 7c-2 2-4 4-6 6-1 2-3 4-5 6l-13 14c-3 3-6 6-9 10-9 10-18 20-27 30-2 2-6 3-8 3-6 0-13 0-21 0l-29 0 0 106 7 0c3 0 5 0 8 0 1 0 2 0 4 0 5 0 11 0 16 0 4 0 9 0 13 0l1 0c4 0 7 2 11 6 7 8 14 16 21 23 8 10 17 20 26 30z m89-110c0 32-15 58-34 58-19 0-34-26-34-58 0-32 15-58 34-58 19 0 34 26 34 58z"/>
<glyph glyph-name="vol-3" unicode="&#57361;" d="M304 254c-2-13-3-26-5-39-2-12-6-24-10-36-3-10-1-17 5-22 12-9 26-5 31 9 14 37 19 75 15 115-3 21-8 43-16 63-4 12-18 16-28 9-8-5-10-13-7-24 9-24 14-49 15-75m-195 98c-2-2-5-4-8-4-7 0-13 0-18 0-2 0-4 0-7 0-2 0-4 0-6 0-23 0-41-16-41-37 0-44 0-65 0-113 1-17 15-33 32-35 8-1 16-1 23-1l1 0c5 0 11 0 16 0 3 0 6-2 8-4 15-15 35-37 54-58 8-9 18-14 30-14 4 0 8 1 12 2 16 6 23 19 23 39l0 162c0 31 2 94 2 95 0 19-9 31-25 37-11 5-29 4-42-10-20-23-37-42-54-59z m66 14l15 18 0-259-4 7c-2 2-4 4-6 6-1 2-3 4-5 6l-13 14c-3 3-6 6-9 10-9 10-18 20-27 30-2 2-6 3-8 3-6 0-13 0-21 0l-29 0 0 106 7 0c3 0 5 0 8 0 1 0 2 0 4 0 5 0 11 0 16 0 4 0 9 0 13 0l1 0c4 0 7 2 11 6 7 8 14 16 21 23 8 10 17 20 26 30z m245-112c-1 6-1 11-2 16-1 11-2 23-3 35-4 25-12 51-22 76-3 6-8 11-14 12-7 2-13 1-19-4-8-6-10-15-5-26 13-34 21-67 21-102 1-39-6-78-22-116-2-6-3-13 0-18 2-5 6-9 12-11 2-1 5-2 8-2 8 0 16 6 20 16 16 39 25 81 26 124z m-156 2c0 32-15 58-34 58-19 0-34-26-34-58 0-32 15-58 34-58 19 0 34 26 34 58z"/>
<glyph glyph-name="vol-4" unicode="&#57362;" d="M304 254c-2-13-3-26-5-39-2-12-6-24-10-36-3-10-1-17 5-22 12-9 26-5 31 9 14 37 19 75 15 115-3 21-8 43-16 63-4 12-18 16-28 9-8-5-10-13-7-24 9-24 14-49 15-75m-195 98c-2-2-5-4-8-4-7 0-13 0-18 0-2 0-4 0-7 0-2 0-4 0-6 0-23 0-41-16-41-37 0-44 0-65 0-113 1-17 15-33 32-35 8-1 16-1 23-1l1 0c5 0 11 0 16 0 3 0 6-2 8-4 15-15 35-37 54-58 8-9 18-14 30-14 4 0 8 1 12 2 16 6 23 19 23 39l0 162c0 31 2 94 2 95 0 19-9 31-25 37-11 5-29 4-42-10-20-23-37-42-54-59z m66 14l15 18 0-259-4 7c-2 2-4 4-6 6-1 2-3 4-5 6l-13 14c-3 3-6 6-9 10-9 10-18 20-27 30-2 2-6 3-8 3-6 0-13 0-21 0l-29 0 0 106 7 0c3 0 5 0 8 0 1 0 2 0 4 0 5 0 11 0 16 0 4 0 9 0 13 0l1 0c4 0 7 2 11 6 7 8 14 16 21 23 8 10 17 20 26 30z m245-112c-1 6-1 11-2 16-1 11-2 23-3 35-4 25-12 51-22 76-3 6-8 11-14 12-7 2-13 1-19-4-8-6-10-15-5-26 13-34 21-67 21-102 1-39-6-78-22-116-2-6-3-13 0-18 2-5 6-9 12-11 2-1 5-2 8-2 8 0 16 6 20 16 16 39 25 81 26 124z m-156 2c0 32-15 58-34 58-19 0-34-26-34-58 0-32 15-58 34-58 19 0 34 26 34 58z m173-186c-4 0-7 1-10 3-10 5-13 18-8 27 45 79 61 208-4 322-6 9-3 22 7 27 10 6 22 2 27-7 74-127 55-273 5-362-4-6-10-10-17-10z"/>
<glyph glyph-name="vol-x-0" unicode="&#57363;" d="M209 196l-57 58 56 57c9 9 9 22 0 31-8 8-22 8-30-1l-57-57-57 58c-8 8-22 8-30-1-9-8-9-22 0-30l57-58-53-58c-9-9-9-22 0-31 8-8 22-8 30 0l54 58 57-57c8-8 22-8 30 0 9 9 8 23 0 31z"/>
<glyph glyph-name="vol-x-1" unicode="&#57364;" d="M248 305l-48-48 48-50c10 10 16 28 16 49 0 21-6 39-16 49z m-39-109l-57 58 56 57c9 9 9 22 0 31-8 8-22 8-30-1l-57-57-57 58c-8 8-22 8-30-1-9-8-9-22 0-30l57-58-53-58c-9-9-9-22 0-31 8-8 22-8 30 0l54 58 57-57c8-8 22-8 30 0 9 9 8 23 0 31z"/>
<glyph glyph-name="vol-x-2" unicode="&#57365;" d="M304 254c-2-13-3-26-5-39-2-12-6-24-10-36-3-10-1-17 5-22 12-9 26-5 31 9 14 37 19 75 15 115-3 21-8 43-16 63-4 12-18 16-28 9-8-5-10-13-7-24 9-24 14-49 15-75m-56 51l-48-48 48-50c10 10 16 28 16 49 0 21-6 39-16 49z m-39-109l-57 58 56 57c9 9 9 22 0 31-8 8-22 8-30-1l-57-57-57 58c-8 8-22 8-30-1-9-8-9-22 0-30l57-58-53-58c-9-9-9-22 0-31 8-8 22-8 30 0l54 58 57-57c8-8 22-8 30 0 9 9 8 23 0 31z"/>
<glyph glyph-name="vol-x-3" unicode="&#57366;" d="M304 254c-2-13-3-26-5-39-2-12-6-24-10-36-3-10-1-17 5-22 12-9 26-5 31 9 14 37 19 75 15 115-3 21-8 43-16 63-4 12-18 16-28 9-8-5-10-13-7-24 9-24 14-49 15-75m116 0c-1 6-1 11-2 16-1 11-2 23-3 35-4 25-12 51-22 76-3 6-8 11-14 12-7 2-13 1-19-4-8-6-10-15-5-26 13-34 21-67 21-102 1-39-6-78-22-116-2-6-3-13 0-18 2-5 6-9 12-11 2-1 5-2 8-2 8 0 16 6 20 16 16 39 25 81 26 124z m-172 51l-48-48 48-50c10 10 16 28 16 49 0 21-6 39-16 49z m-39-109l-57 58 56 57c9 9 9 22 0 31-8 8-22 8-30-1l-57-57-57 58c-8 8-22 8-30-1-9-8-9-22 0-30l57-58-53-58c-9-9-9-22 0-31 8-8 22-8 30 0l54 58 57-57c8-8 22-8 30 0 9 9 8 23 0 31z"/>
<glyph glyph-name="vol-x-4" unicode="&#57367;" d="M304 254c-2-13-3-26-5-39-2-12-6-24-10-36-3-10-1-17 5-22 12-9 26-5 31 9 14 37 19 75 15 115-3 21-8 43-16 63-4 12-18 16-28 9-8-5-10-13-7-24 9-24 14-49 15-75m116 0c-1 6-1 11-2 16-1 11-2 23-3 35-4 25-12 51-22 76-3 6-8 11-14 12-7 2-13 1-19-4-8-6-10-15-5-26 13-34 21-67 21-102 1-39-6-78-22-116-2-6-3-13 0-18 2-5 6-9 12-11 2-1 5-2 8-2 8 0 16 6 20 16 16 39 25 81 26 124z m17-184c-4 0-7 1-10 3-10 5-13 18-8 27 45 79 61 208-4 322-6 9-3 22 7 27 10 6 22 2 27-7 74-127 55-273 5-362-4-6-10-10-17-10z m-189 235l-48-48 48-50c10 10 16 28 16 49 0 21-6 39-16 49z m-39-109l-57 58 56 57c9 9 9 22 0 31-8 8-22 8-30-1l-57-57-57 58c-8 8-22 8-30-1-9-8-9-22 0-30l57-58-53-58c-9-9-9-22 0-31 8-8 22-8 30 0l54 58 57-57c8-8 22-8 30 0 9 9 8 23 0 31z"/>
<glyph glyph-name="share-ext" unicode="&#57368;" d="M135 133c0 71 0 127 0 198 51 0 85 0 136 0 1 0 1-1 1-1-4-3-7-7-11-10-4-4-8-10-13-12-5-2-11 0-17 0-28 0-39 0-67 0-2 0-3 0-5 0 0-56 0-96 0-151 55 0 94 0 149 0 0 2 0 3 0 5 0 32 0 50 0 82 0 3 1 5 3 7 7 7 14 13 20 20 0-51 0-87 0-138-71 0-125 0-196 0z m202 222c-21 0-42 0-64 0 0 8 0 16 0 24 35 0 71 0 106 0 0-36 0-71 0-107-8 0-15 0-23 0 0 22 0 43 0 65-49-49-97-97-145-145-6 7-12 12-17 17 48 48 96 97 144 145 0 0 0 1-1 1z"/>
<glyph glyph-name="ellipsis" unicode="&#57369;" d="M174 232c-14 0-25 10-25 24 0 13 11 24 25 24 13 0 24-11 24-24 0-14-11-24-24-24z m78 0c-14 0-25 10-25 24 0 13 11 24 25 24 13 0 24-11 24-24 0-14-11-24-24-24z m78 0c-14 0-25 10-25 24 0 13 11 24 25 24 13 0 24-11 24-24 0-14-11-24-24-24z"/>
<glyph glyph-name="check" unicode="&#57370;" d="M256 426c95 0 172-77 171-173 0-93-77-169-171-169-95 0-171 77-171 173 0 93 78 169 171 169z m-23-192c-2 3-3 5-4 6-13 13-26 26-39 39-10 11-26 12-36 2-10-10-10-25 1-36 20-21 40-41 60-61 11-10 26-10 36 0 36 35 71 71 106 107 4 3 7 9 8 14 2 10-3 21-13 26-10 4-21 3-29-5-28-29-57-57-85-86-2-1-3-3-5-6z"/>
<glyph glyph-name="sliders" unicode="&#38;" d="M185 371l-35 0c-9 0-17-8-17-16 0-9 8-16 17-16l35 0z m190 0l-99 0 0-32 99 0c9 0 17 7 17 16 0 8-8 16-17 16m-163-195l-62 0c-9 0-17-8-17-17 0-9 8-16 17-16l62 0z m163 0l-71 0 0-33 71 0c9 0 17 7 17 16 0 9-8 17-17 17m-63 95l-162 0c-9 0-17-6-17-15 0-9 8-16 17-16l162 0z m-63 120c2 0 4-1 6-4 2-1 2-4 2-8l0-48c0-4 0-7-2-8-2-3-4-4-6-4l-36 0c-2 0-4 1-6 4-2 2-2 5-2 8l0 48c0 4 0 6 2 8 2 3 4 4 6 4z m126-100c2 0 4-1 6-4 2-2 2-5 2-8l0-48c0-2-1-5-2-8-2-2-4-4-6-4l-34 0c-2 0-4 2-6 4-2 2-3 5-3 8l0 48c0 4 1 7 3 8 2 3 4 4 6 4z m-100-96c2 0 5-1 7-3 2-3 2-6 2-9l0-48c0-2 0-5-2-8-2-3-4-4-7-4l-36 0c-3 0-5 1-6 4-2 2-3 5-3 8l0 48c0 4 1 6 3 9 1 2 4 3 6 3z"/>
<glyph glyph-name="polyline" unicode="&#57371;" d="M150 90c1 0 3 0 5-1 4-1 8 0 12 2 3 1 6 5 7 9 1 3 0 8-2 11-2 3-5 6-9 7-2 1-4 1-5 1-4 1-8 1-12-1-3-2-6-6-7-9-1-4-1-8 2-12 2-3 5-6 9-7z m-55 36c1-2 2-3 3-5 1 0 1-1 2-1 0-1 1-2 2-2 3-3 6-5 10-5 2 0 4 1 6 2 2 0 4 1 5 3 2 3 4 6 4 10 0 4-1 8-4 11-1 1-3 2-4 4 1-1 2-2 3-3-1 0-1 1-1 1-2 3-6 6-9 7-2 1-4 1-6 0-2 0-4 0-6-2-3-2-6-5-7-9-1-4 0-7 2-11z m-15 64c0-3 0-6 0-8 0-4 1-8 4-11 3-3 7-5 11-4 8 0 15 6 15 15 0 2 0 5 0 8 0 4-2 8-5 10-2 3-6 5-10 5-9-1-15-7-15-15z m22 87c-5-10-9-21-12-32-2-8 3-17 10-19 9-1 16 3 19 11 2 8 5 17 9 25 3 7 2 16-6 20-6 4-17 2-20-5z m59 82c-12-12-23-24-33-38-5-6-2-16 5-20 8-5 16-1 20 5 8 11 19 22 29 32 6 5 6 15 0 21-6 6-15 5-21 0z m104-63c-19-8-25-31-20-50 4-21 23-33 43-34 21-2 40 10 52 26 12 18 14 41 8 62-7 20-21 37-40 47-19 9-42 11-63 4-39-12-65-49-69-89-3-38 15-77 45-100 33-27 78-34 119-23 80 23 131 113 109 193-10 37-37 66-72 81-40 16-88 11-128-3-10-3-21-8-31-13-7-3-9-13-5-20 4-7 13-9 20-5 32 15 68 24 104 20 15-1 30-6 42-14 3-1 6-3 9-6 2-1-2 2 0 0 1 0 2-1 2-2 2-1 4-2 5-4 3-2 5-5 7-7 0 0 4-5 2-3 1-1 2-3 3-4 2-3 4-6 6-9 1-2 2-4 2-6 1 0 1-1 1-2-1 3 1-1 1-1 3-8 5-16 6-25 0 2 0-1 0-1 1-1 1-2 1-3 0-2 0-4 0-6 0-5 0-10 0-15-1-7-2-14-4-23-4-16-10-29-20-43-1-2-2-4-4-5 0-1-1-3 0-1-1-1-1-2-2-2-2-3-5-7-8-9-2-3-5-5-8-8-1-1-3-2-4-4 0 0-2-2-1 0-1-1-2-2-3-3-14-10-26-15-41-19-9-2-14-3-23-3-9-1-17 0-23 1-16 3-31 9-43 19-1 1-5 4-7 7-3 2-6 5-8 8 0 1-2 3-1 1 0 0 0 1-1 2-1 1-2 3-3 5-2 3-4 6-6 10 0 1-2 5-1 3-1 1-2 3-2 5-2 4-3 8-3 12-1 1-1 3-1 5-1 2 0-2 0 0 0 1 0 2-1 3 0 4 0 8 0 12 0 2 1 4 1 6 0 3 0 1 0 0 0 1 0 2 0 3 1 4 2 8 3 11 1 2 1 4 2 5 0 0 1 3 0 2 0-2 1 1 1 1 2 4 4 7 6 11 1 1 2 2 3 4-1-2 0 0 0 0 1 1 2 2 2 2 3 3 6 6 9 9 0 0 2 1 0 0 1 1 2 1 3 2 1 1 3 2 5 3 3 2 7 4 10 5 8 3 14 4 22 4 2 0 5 0 7-1-2 1 2 0 3 0 2-1 4-1 5-2 1 0 2-1 3-1-2 1 1 0 1-1 4-1 7-3 10-5-1 1 2-2 3-2 1-2 3-4 4-5 1-1 2-2 2-3 0 1-1 2 1 0 1-2 2-3 3-5 1-2 2-3 3-5 1-3 0-2 0-1 0-1 1-3 1-4 1-2 1-4 2-5 0-1 0-2 0-4 0 2 1 0 1 0 0-3 0-5 0-7 0-1 0-2 0-2 0-1 0-1 0 0 0-1 0-2-1-2 0-2 0-4-1-6 0-1-1-2-1-3-1-2 1 2 0 0-1-2-2-3-3-5 0-1-1-1-1-2-3-4 1 1-1-1-1-1-2-3-3-4-1 0-5-4-3-2-2-1-3-2-5-3 0 0-1-1-2-1-1-1 2 0-1-1-2 0-4-1-6-1 3 1-1 0-2 0-1 0-3 0-4 0 3 0 0 0 0 0-1 1-3 1-4 1 0 0-3 1-1 1 2-1 0 0 0 0-2 1-4 2-5 3 2-2-1 1-2 2 2-2-1 1-1 2-2 2 0 1 0 0 0 1-1 2-1 2 0 1 0 2 0 2-1 2 0-2 0 0 0 1-1 3-1 4 0 1 1 2 1 3 0-3 0-1 0 0 0 1 0 2 1 3 0 0 1 3 0 1-1-2 1 2 2 2-2-2 0 1 1 2-2-2 2 0 3 1 7 3 9 14 5 20-5 8-13 10-21 6z"/>
<glyph glyph-name="source" unicode="&#57372;" d="M397 222c-40 0-73-28-76-67-41 9-57 54-59 60-16 47-46 69-90 69-4 0-7 0-10-1l-3 0c-1 0-1 0-2 0l-2-2c-7-4-27-13-42-13-27 0-49 22-49 49 0 26 22 48 49 48 27 0 49-22 49-48 0-3 0-8 0-8 0-1 0-1 0-2 1-1 2-1 2-1l181-1c1 0 2 1 2 1 1 1 1 2 1 2 0 0 0 6 0 9 0 26 22 48 49 48 27 0 49-22 49-48 0-27-22-49-49-49-11 0-21 4-30 10 0 1-1 1-1 1-1 0-1 0-1 0-1 0-1-1-2-1l-14-19c0-1-1-1-1-2 1-1 1-1 1-2 14-10 31-16 48-16 43 0 78 35 78 77 0 43-35 77-78 77-41 0-74-26-77-65l-131 0c-3 39-36 66-76 66-43 0-78-35-78-77 0-43 35-77 78-77 21 0 45 9 54 15 2 0 3 0 4 0 31 0 51-14 63-49 1-3 26-79 101-79l0 0 12 0c1 0 2 1 2 1 1 1 1 1 1 2 0 0-1 16-1 18 0 26 21 45 48 45 27 0 49-21 49-48 0-26-22-48-49-48-11 0-21 3-30 10 0 0-1 0-1 0 0 0-1 0-1 0 0 0-1 0-2-1l-14-18c0-1-1-2 0-2 0-1 0-2 1-2 13-10 30-16 47-16 43 0 78 35 78 77 0 43-35 77-78 77z"/>
<glyph glyph-name="playback-play" unicode="&#57373;" d="M128 416l256-160-256-160z"/>
<glyph glyph-name="stop-square" unicode="&#57374;" d="M384 128l-256 0 0 256 256 0z"/>
<glyph glyph-name="avatar-t-pose" unicode="&#57375;" d="M274 70c-1 0-1 0-1 0-12-1-14 2-14 12 0 0 0 133 0 135-3 0-6 0-9 0-2-10 1-140 1-140-1-7-2-8-9-8-2 0-5 0-7 0-8 109-16 188-16 188 0 24 0 46 0 66-1-1-33 0-44 1-15 0-62 1-79 1-8 0-14 3-18 10-1 2-2 4-4 6 8 1 15 1 22 1 35 2 99 9 100 13 14 10 23 10 36 10 15 0 31 0 46-1 11-1 24 3 37-10 20-10 81-11 123-11 0 0 1 0 1-1-4-11-12-17-25-16-29-1-77-3-127-1 1-20 1-42 2-66 0 0-6-59-15-189z m-13 372c16-6 14-20 13-32 0-5-1-10-1-14-2-11-10-18-20-18-11 0-19 8-20 18-1 6-1 13-2 20-1 7 3 13 11 15 10 3 10 3 19 11z"/>
<glyph glyph-name="check-1" unicode="&#57376;" d="M233 234c-2 3-3 5-4 6-13 13-26 26-39 39-10 11-26 12-36 2-10-10-10-25 1-36 20-21 40-41 60-61 11-10 26-10 36 0 36 35 71 71 106 107 4 3 7 9 8 14 2 10-3 21-13 26-10 4-21 3-29-5-28-29-57-57-85-86-2-1-3-3-5-6z"/>
<glyph glyph-name="exchange" unicode="&#57377;" d="M315 344c0 8 0 15 0 22 0 7 0 13 1 20 0 2 2 5 3 6 3 1 6 0 8-1 1 0 2-1 2-1 21-21 41-42 62-62 5-5 5-8 0-13-21-21-42-42-62-63-5-4-11-4-13 1-1 1-1 3-1 5 0 11 0 22 0 33 0 2 0 3 0 6-2 0-4 0-5 0-61 0-122 0-183 0-9 0-10 1-10 10 0 10 0 19 0 29 0 6 2 8 8 9 1 0 2 0 4 0 60 0 120 0 180 0 2-1 4-1 6-1z m-116-121c2 0 4 0 6 0 61 0 122 0 182 0 9 0 11-1 11-10 0-10 0-19 0-29 0-6-3-8-9-9-1 0-3 0-4 0-60 0-120 0-180 0-2 0-4 0-6 0 0-2 0-3 0-5 0-11 0-23 0-34 0-4-1-6-4-8-4-1-6 0-9 2-21 22-42 43-64 64-4 4-4 7 1 12 21 21 42 42 63 63 3 3 6 4 9 2 3-1 4-4 4-7 0-12 0-24 0-35 0-2 0-3 0-6z"/>
<glyph glyph-name="hfc" unicode="&#57378;" d="M370 142c-12-23-30-42-51-55-12-8-25-14-39-18-38-12-79-7-115 12-35 19-61 51-72 90-12 38-7 79 12 114 12 23 29 42 50 55 7 5 14 8 22 12l1-229c10-6 22-11 34-14l-1 251c13 2 31 2 50 2l1-254c12 2 24 6 34 11l0 244c37 0 72-1 84-1l7 33c-15 0-52 1-91 1l0 57 166 1 0 34-201-1 1-91c-19 0-37-1-51-2l-1 93-34-1 1-98c-44-15-80-45-102-86-24-44-29-94-15-141 14-47 46-86 89-110 44-23 94-29 141-15 17 6 33 13 48 22 26 17 47 40 62 68 8 14 13 28 17 44-11 2-22 4-33 7-3-12-8-24-14-35z"/>
<glyph glyph-name="home-1" unicode="&#57379;" d="M155 273c-5 0-10 2-14 7-5 7-3 17 5 23l29 19 84 59 109-79c8-5 9-15 4-22-5-8-16-9-23-4l-90 65-65-46-30-19c-3-2-6-3-9-3z m84-78c0 12 10 21 21 21 11 0 21-10 21-21l0-60 75 0 0 89-32 24-64 46-68-46-33-21 0-92 80 0 0 60z"/>
<glyph glyph-name="private-key" unicode="&#57380;" d="M238 263c-30 43-16 96 20 121 34 25 82 21 112-8 30-31 33-79 8-114-25-34-77-48-120-18-21-21-41-41-61-61 2-3 5-6 7-8 6-6 12-12 18-18 3-3 3-5 0-7-3-3-6-6-9-9-3-3-5-3-7 0-5 5-9 9-14 14-5-5-9-10-14-15 4-4 9-9 14-14 2-2 2-4 0-6-4-3-7-7-10-10-3-2-4-2-6 0-14 14-28 28-42 42-4 3-2 5 0 7 33 33 67 66 100 99 1 2 3 3 4 5z m131 51c0 34-26 61-61 61-33 0-60-27-61-60 0-35 27-61 62-62 33 0 60 28 60 61z"/>
<glyph glyph-name="security-pic" unicode="&#57382;" d="M365 406l-212 0c-18 0-33-14-33-32l0-91c9 5 18 11 27 15l0 76c0 3 3 5 6 5l212 0c3 0 5-2 5-5l0-212c0-3-2-6-5-6l-106 0 0-27 106 0c18 0 32 15 32 33l0 212c0 18-14 32-32 32z m-153-209l0 16c0 28-23 51-51 51-28 0-51-23-51-51l0-16c-10-2-18-11-18-22l0-39c0-12 9-22 21-22l95 0c12 0 21 10 21 22l0 39c1 11-7 20-17 22z m-51 44c15 0 27-12 27-28l0-16-55 0 0 16c0 16 13 28 28 28z m183 49l-66 49-83-50c7-4 14-10 19-17l62 37 68-50 0 31z m-114-44l114 0 0-26-114 0z"/>
<glyph glyph-name="wallet" unicode="&#57383;" d="M400 400c-3 10-8 19-15 24-7 5-15 8-24 7l-227 0c-19 0-35-15-35-34l0-58c-19-2-34-18-34-37l0-95c0-19 15-35 34-37l0-53c0-19 16-34 35-34l228 0c20 0 30 16 38 31l0 1c1 2 21 53 21 139 0 83-19 140-21 146z m-309-193l0 95c0 6 5 11 11 11l86 0c6 0 11-5 11-11l0-95c0-6-5-11-11-11l-86 0c-6 0-11 5-11 11z m285-82c-8-16-11-16-14-16l-228 0c-5 0-9 4-9 8l0 53 63 0c21 0 37 17 37 37l0 95c0 20-16 37-37 37l-63 0 0 58c0 4 4 8 9 8l228 0c3 0 10 1 13-13l0 0 0-1c1 0 20-55 20-137 0-77-17-124-19-129z"/>
<glyph glyph-name="send" unicode="&#57384;" d="M391 376c4-4 4-10 2-16-7-21-14-42-22-63-21-63-43-125-65-188-1-4-4-7-6-10-8-6-17-4-22 5-15 28-30 56-44 85-1 1 0 3 0 4 18 21 35 43 53 64 5 6 10 12 15 18 4 6 5 10 1 14-4 4-8 3-14-1-18-15-36-30-54-44-9-8-19-16-28-24-1-1-3-1-4 0-29 14-57 29-85 44-6 3-8 8-8 14 1 7 5 11 11 13 36 13 72 25 107 37 49 17 97 34 145 51 7 2 13 2 18-3z"/>
<glyph glyph-name="password" unicode="&#57385;" d="M104 267l0 41 22 0 0-41 35 20 11-19-35-20 35-20-11-20-35 20 0-40-22 0 0 40-35-20-11 20 35 20-35 20 11 19z m136 0l0 41 23 0 0-41 35 20 11-19-35-20 35-20-11-20-35 20 0-40-23 0 0 40-35-20-11 20 35 20-35 20 11 19z m137 0l0 41 23 0 0-41 34 20 12-19-35-20 35-20-12-20-35 20 0-40-22 0 0 40-35-20-11 20 35 20-35 20 11 19z"/>
<glyph glyph-name="rez" unicode="&#57381;" d="M373 321c-2 5-6 8-11 8l-49 8 55 61c4 4 5 9 3 14-2 5-7 8-12 8 0 0 0 0 0 0l-114-1c-5-1-10-4-12-9l-54-136c-1-4-1-8 1-11 2-4 6-6 9-7l38-5-54-136c-2-6 0-13 6-16 2-1 4-2 7-2 3 0 7 2 10 5l175 206c3 4 3 9 2 13z"/>
<glyph glyph-name="keyboard-collapse" unicode="&#57387;" d="M373 249l-26 0 0 25 26 0z m-35 0l-27 0 0 25 27 0z m-36 0l-27 0 0 25 27 0z m-36 0l-26 0 0 25 26 0z m-35 0l-27 0 0 25 27 0z m-36 0l-27 0 0 25 27 0z m-36 0l-26 0 0 25 26 0z m224-1l18 0c7 0 13 6 13 13 0 7-6 13-13 13l-18 0m-262 0l-17 0c-7 0-13-6-13-13 0-7 6-13 13-13l17 0m252 39l-31 0 0 25 31 0z m-42 0l-31 0 0 25 31 0z m-41 0l-32 0 0 25 32 0z m-42 0l-32 0 0 25 32 0z m-42 0l-32 0 0 25 32 0z m-41 0l-32 0 0 25 32 0z m218-1l18 0c7 0 13 6 13 13 0 8-6 14-13 14l-18 0m-262 0l-17 0c-7 0-13-6-13-14 0-7 6-13 13-13l17 0m288-124l-315 0c-33 0-59 28-59 61l0 76c0 34 26 61 59 61l315 0c33 0 59-27 59-61l0-76c1-33-26-61-59-61z m-315 172c-18 0-33-16-33-34l0-77c0-19 15-34 33-34l315 0c18 0 33 15 33 34l0 77c0 19-15 34-33 34z m248-99l32 0 0-25-32 0z m-41 0l31 0 0-25-31 0z m-42 0l31 0 0-25-31 0z m-43 0l32 0 0-25-32 0z m-42 0l32 0 0-25-32 0z m-41 0l31 0 0-25-31 0z m250-26l18 0c7 0 13 6 13 14 0 7-6 13-13 13l-18 0m-262 0l-17 0c-7 0-13-6-13-13 0-8 6-13 13-13l17 0m81-82l50-50 53 54-107 0"/>
<glyph glyph-name="image" unicode="&#57386;" d="M257 428c52 0 104 0 156 0 24 0 37-13 37-37 1-90 1-179 0-269 0-25-13-38-39-38-103 0-207 0-311 0-26 0-39 13-39 40 0 88 0 176 0 263 0 28 13 41 41 41 51 0 103 0 155 0z m167-263c0 7 0 10 0 14 0 69 0 138 0 206 0 17 0 17-17 17-101 0-202 0-303 0-16 0-17-1-17-17 0-58 0-115 0-173 0-3 0-7 0-12 8 3 14 6 19 9 17 8 30 7 44-6 5-5 10-10 15-15 5-7 11-8 19-4 40 21 81 41 121 61 19 10 31 8 46-7 9-9 18-18 27-27 15-15 29-29 46-46z m-328-54c7 0 11-1 15-1 98 0 197 0 296 0 6 0 14 2 16 6 5 7 0 14-6 20-27 26-54 53-80 80-8 9-15 9-26 4-67-35-135-68-203-102-3-2-6-4-12-7z m-8 26c21 10 40 20 63 31-8 7-14 12-20 17-2 2-7 3-10 3-30-9-36-17-34-48 0 0 0-1 1-3z m134 169c1-25-21-46-46-46-25-1-46 20-47 46 0 25 21 46 47 47 25 0 46-21 46-47z m-46 22c-12 0-22-9-22-21 0-13 9-22 21-23 13 0 23 9 23 22 0 13-10 22-22 22z"/>
</font></defs></svg>

After

Width:  |  Height:  |  Size: 78 KiB

View file

@ -12,7 +12,7 @@
<body>
<div class="container">
<h1>HiFi Glyphs</h1>
<p class="small">This font was created in<a href="http://highfidelity.com/">High Fidelity</a></p>
<p class="small">This font was created for use in<a href="http://highfidelity.io/">High Fidelity</a></p>
<h2>CSS mapping</h2>
<ul class="glyphs css-mapping">
<li>
@ -520,8 +520,52 @@
<input type="text" readonly="readonly" value="avatar-t-pose">
</li>
<li>
<div class="icon icon-check-2-01"></div>
<input type="text" readonly="readonly" value="check-2-01">
<div class="icon icon-check-1"></div>
<input type="text" readonly="readonly" value="check-1">
</li>
<li>
<div class="icon icon-exchange"></div>
<input type="text" readonly="readonly" value="exchange">
</li>
<li>
<div class="icon icon-hfc"></div>
<input type="text" readonly="readonly" value="hfc">
</li>
<li>
<div class="icon icon-home-1"></div>
<input type="text" readonly="readonly" value="home-1">
</li>
<li>
<div class="icon icon-private-key"></div>
<input type="text" readonly="readonly" value="private-key">
</li>
<li>
<div class="icon icon-security-pic"></div>
<input type="text" readonly="readonly" value="security-pic">
</li>
<li>
<div class="icon icon-wallet"></div>
<input type="text" readonly="readonly" value="wallet">
</li>
<li>
<div class="icon icon-send"></div>
<input type="text" readonly="readonly" value="send">
</li>
<li>
<div class="icon icon-password"></div>
<input type="text" readonly="readonly" value="password">
</li>
<li>
<div class="icon icon-rez"></div>
<input type="text" readonly="readonly" value="rez">
</li>
<li>
<div class="icon icon-keyboard-collapse"></div>
<input type="text" readonly="readonly" value="keyboard-collapse">
</li>
<li>
<div class="icon icon-image"></div>
<input type="text" readonly="readonly" value="image">
</li>
</ul>
<h2>Character mapping</h2>
@ -1034,6 +1078,50 @@
<div data-icon="&#xe020;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe020;">
</li>
<li>
<div data-icon="&#xe021;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe021;">
</li>
<li>
<div data-icon="&#xe022;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe022;">
</li>
<li>
<div data-icon="&#xe023;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe023;">
</li>
<li>
<div data-icon="&#xe024;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe024;">
</li>
<li>
<div data-icon="&#xe026;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe026;">
</li>
<li>
<div data-icon="&#xe027;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe027;">
</li>
<li>
<div data-icon="&#xe028;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe028;">
</li>
<li>
<div data-icon="&#xe029;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe029;">
</li>
<li>
<div data-icon="&#xe025;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe025;">
</li>
<li>
<div data-icon="&#xe02b;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe02b;">
</li>
<li>
<div data-icon="&#xe02a;" class="icon"></div>
<input type="text" readonly="readonly" value="&amp;#xe02a;">
</li>
</ul>
</div>
<script>(function() {

View file

@ -416,6 +416,39 @@
.icon-avatar-t-pose:before {
content: "\e01f";
}
.icon-check-2-01:before {
.icon-check-1:before {
content: "\e020";
}
.icon-exchange:before {
content: "\e021";
}
.icon-hfc:before {
content: "\e022";
}
.icon-home-1:before {
content: "\e023";
}
.icon-private-key:before {
content: "\e024";
}
.icon-security-pic:before {
content: "\e026";
}
.icon-wallet:before {
content: "\e027";
}
.icon-send:before {
content: "\e028";
}
.icon-password:before {
content: "\e029";
}
.icon-rez:before {
content: "\e025";
}
.icon-keyboard-collapse:before {
content: "\e02b";
}
.icon-image:before {
content: "\e02a";
}

View file

@ -342,7 +342,7 @@
<p>In High Fidelity, your private keys are used to securely access the contents of your Wallet and Purchases.</p>
<hr>
<h3>Where are my private keys stored?"</h3>
<h3>Where are my private keys stored?</h3>
<p>By default, your private keys are only stored on your hard drive in High Fidelity Interface's <code>AppData</code> directory.</p>
<p>Here is the file path of your hifikey - you can browse to it using your file explorer.</p>
<div class="alert"> <code>HIFIKEY_PATH_REPLACEME</code> </div>

View file

@ -0,0 +1,9 @@
<?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" 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 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path d="M348.5,162.5c1.7,0,3,1.4,3,3v181.4c0,1.7-1.3,3-3,3H167.2c-1.6,0-3-1.3-3-3V165.5c0-1.6,1.4-3,3-3H348.5 M348.5,145.5
H167.2c-11,0-20,9-20,20v181.4c0,11,9,20,20,20h181.4c11,0,20-9,20-20V165.5C368.5,154.5,359.6,145.5,348.5,145.5L348.5,145.5z"/>
<rect x="161.6" y="253.6" width="96.3" height="96.3"/>
<rect x="256.1" y="159.8" width="95.4" height="95.4"/>
</svg>

After

Width:  |  Height:  |  Size: 717 B

View file

@ -0,0 +1,23 @@
<?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 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M256.5,83.7c52.1,0,104.2-0.1,156.3,0.1c23.7,0.1,37.6,13.5,37.6,37.2c0.2,89.6,0.3,179.2,0,268.7
c-0.1,25.5-13.6,38.2-39,38.3c-103.8,0.1-207.5,0.1-311.3,0c-25.8,0-38.8-13.5-38.8-39.7c0-87.9,0-175.9,0-263.8
c0-27.7,12.8-40.7,40.2-40.8C153.2,83.6,204.8,83.7,256.5,83.7z M423.7,347c0.4-6.9,0.7-10.5,0.7-14.1c0-68.8,0-137.5,0-206.3
c0-16.2-0.6-16.7-17-16.7c-101.3,0-202.5,0-303.8,0c-15.9,0-16.9,1-16.9,17.2c0,57.5,0,115,0,172.5c0,3.5,0.3,7.1,0.6,12.4
c7.3-3.5,13.2-6.3,19.1-9.2c16.7-8.1,29.8-6.4,43.6,6c5.2,4.7,10.3,9.8,14.7,15.3c5.6,6.9,10.8,7.8,19.1,3.5
c40.3-20.8,81-40.9,121.6-61.1c18.3-9.1,30.6-7.1,45.3,7c9.3,9,18.3,18.2,27.4,27.4C392.7,315.5,407.2,330.2,423.7,347z
M95.6,400.9c7.3,0.4,11.2,0.8,15.1,0.8c98.7,0,197.5,0.1,296.2-0.2c5.6,0,14-1.4,16.2-5.1c4.5-7.4-0.3-14.4-6.5-20.5
c-26.8-26.2-53.5-52.6-79.5-79.6c-8.5-8.9-15.4-9.4-26.1-4c-67.5,34.3-135.3,67.9-203,101.8C104.9,395.7,101.8,397.5,95.6,400.9z
M88.2,375.1c20.6-10.3,40.2-20.1,62.4-31.3c-7.6-6.7-13.2-12.2-19.5-16.9c-2.6-1.9-7-3.3-9.9-2.5c-30,8.1-36,16.8-34,47.6
C87.1,372.4,87.4,372.8,88.2,375.1z"/>
<path class="st0" d="M222.2,205.5c0.3,25.1-20.8,46.5-46,46.8c-25.1,0.3-46.5-20.8-46.8-46.1c-0.3-25.1,20.8-46.5,46.1-46.8
C200.5,159.2,221.9,180.2,222.2,205.5z M176,184.1c-12.3-0.1-21.7,9-21.9,21.3c-0.2,12.3,8.8,21.8,21,22.2
c12.8,0.4,22.5-9.1,22.5-21.9C197.5,193.4,188.3,184.2,176,184.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -256,7 +256,7 @@ Item {
id: octreeCol
spacing: 4; x: 4; y: 4;
StatText {
text: "Engine: " + root.engineFrameTime.toFixed(1) + " ms"
text: "Render Engine: " + root.engineFrameTime.toFixed(1) + " ms"
}
StatText {
text: "Batch: " + root.batchFrameTime.toFixed(1) + " ms"

View file

@ -34,7 +34,7 @@ Item {
height: parent.height
}
FontLoader { id: ralewayRegular; source: pathToFonts + "fonts/Raleway-Regular.ttf"; }
FontLoader { id: ralewayRegular; source: "qrc:/fonts/Raleway-Regular.ttf"; }
Timer {
id: updateList

View file

@ -257,7 +257,11 @@ Item {
id: octreeCol
spacing: 4; x: 4; y: 4;
StatText {
text: "Engine: " + root.engineFrameTime.toFixed(1) + " ms"
text: "Render Engine: " + root.engineFrameTime.toFixed(1) + " ms"
}
StatText {
visible: root.expanded
text: root.renderEngineStats
}
StatText {
text: "Batch: " + root.batchFrameTime.toFixed(1) + " ms"

View file

@ -109,7 +109,7 @@ CheckBox {
contentItem: Text {
id: root
FontLoader { id: ralewaySemiBold; source: pathToFonts + "fonts/Raleway-SemiBold.ttf"; }
FontLoader { id: ralewaySemiBold; source: "qrc:/fonts/Raleway-SemiBold.ttf"; }
font.pixelSize: hifi.fontSizes.inputLabel
font.family: ralewaySemiBold.name
text: checkBox.text

View file

@ -125,7 +125,7 @@ Rectangle {
TextInput {
id: mirrorText
visible: showMirrorText
FontLoader { id: font; source: "../../fonts/FiraSans-Regular.ttf"; }
FontLoader { id: font; source: "qrc:/fonts/FiraSans-Regular.ttf"; }
font.family: font.name
font.pixelSize: 20
verticalAlignment: Text.AlignVCenter
@ -165,7 +165,7 @@ Rectangle {
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
FontLoader { id: hiFiGlyphs; source: pathToFonts + "fonts/hifi-glyphs.ttf"; }
FontLoader { id: hiFiGlyphs; source: "qrc:/fonts/hifi-glyphs.ttf"; }
Column {
id: columnAlpha

View file

@ -14,8 +14,8 @@ import "../styles-uit"
Item {
property int colorScheme: 0;
readonly property var topColor: [ hifi.colors.baseGrayShadow, hifi.colors.faintGray ];
readonly property var bottomColor: [ hifi.colors.baseGrayHighlight, hifi.colors.faintGray ];
readonly property var topColor: [ hifi.colors.baseGrayShadow, hifi.colors.faintGray, "#89858C" ];
readonly property var bottomColor: [ hifi.colors.baseGrayHighlight, hifi.colors.faintGray, "#89858C" ];
// Size
height: colorScheme === 0 ? 2 : 1;

View file

@ -25,7 +25,7 @@ SpinBox {
property color colorLabelInside: hifi.colors.white
property real controlHeight: height + (spinBoxLabel.visible ? spinBoxLabel.height + spinBoxLabel.anchors.bottomMargin : 0)
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
FontLoader { id: firaSansSemiBold; source: "qrc:/fonts/FiraSans-SemiBold.ttf"; }
font.family: firaSansSemiBold.name
font.pixelSize: hifi.fontSizes.textFieldInput
height: hifi.fontSizes.textFieldInput + 13 // Match height of TextField control.

View file

@ -22,6 +22,10 @@ TableView {
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
property bool expandSelectedRow: false
property bool centerHeaderText: false
readonly property real headerSpacing: 3 //spacing between sort indicator and table header title
property var titlePaintedPos: [] // storing extra data position behind painted
// title text and sort indicatorin table's header
signal titlePaintedPosSignal(int column) //signal that extradata position gets changed
model: ListModel { }
@ -69,36 +73,39 @@ TableView {
height: hifi.dimensions.tableHeaderHeight
color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark
RalewayRegular {
id: titleText
x: centerHeaderText ? (parent.width - paintedWidth -
((sortIndicatorVisible &&
sortIndicatorColumn === styleData.column) ?
(titleSort.paintedWidth / 5 + tableView.headerSpacing) : 0)) / 2 :
hifi.dimensions.tablePadding
text: styleData.value
size: hifi.fontSizes.tableHeading
font.capitalization: Font.AllUppercase
color: hifi.colors.baseGrayHighlight
horizontalAlignment: (centerHeaderText ? Text.AlignHCenter : Text.AlignLeft)
anchors {
left: parent.left
leftMargin: hifi.dimensions.tablePadding
right: parent.right
rightMargin: hifi.dimensions.tablePadding
verticalCenter: parent.verticalCenter
}
anchors.verticalCenter: parent.verticalCenter
}
//actual image of sort indicator in glyph font only 20% of real font size
//i.e. if the charachter size set to 60 pixels, actual image is 12 pixels
HiFiGlyphs {
id: titleSort
text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn
color: hifi.colors.darkGray
opacity: 0.6;
size: hifi.fontSizes.tableHeadingIcon
anchors {
left: titleText.right
leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 15 : 10)
right: parent.right
rightMargin: hifi.dimensions.tablePadding
verticalCenter: titleText.verticalCenter
}
anchors.verticalCenter: titleText.verticalCenter
anchors.left: titleText.right
anchors.leftMargin: -(hifi.fontSizes.tableHeadingIcon / 2.5) + tableView.headerSpacing
visible: sortIndicatorVisible && sortIndicatorColumn === styleData.column
onXChanged: {
titlePaintedPos[styleData.column] = titleText.x + titleText.paintedWidth +
paintedWidth / 5 + tableView.headerSpacing*2
titlePaintedPosSignal(styleData.column)
}
}
Rectangle {
@ -152,7 +159,7 @@ TableView {
color: styleData.selected
? hifi.colors.primaryHighlight
: tableView.isLightColorScheme
? (styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd)
: (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd)
? (styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd)
: (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd)
}
}

View file

@ -17,7 +17,7 @@ TextEdit {
property real size: 32
FontLoader { id: ralewaySemiBold; source: "../../fonts/Raleway-SemiBold.ttf"; }
FontLoader { id: ralewaySemiBold; source: "qrc:/fonts/Raleway-SemiBold.ttf"; }
font.family: ralewaySemiBold.name
font.pointSize: size
verticalAlignment: Text.AlignVCenter

View file

@ -34,11 +34,10 @@ TextField {
placeholderText: textField.placeholderText
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
FontLoader { id: hifiGlyphs; source: "../../fonts/hifi-glyphs.ttf"; }
font.family: firaSansSemiBold.name
FontLoader { id: firaSansRegular; source: "qrc:/fonts/FiraSans-Regular.ttf"; }
FontLoader { id: hifiGlyphs; source: "qrc:/fonts/hifi-glyphs.ttf"; }
font.family: firaSansRegular.name
font.pixelSize: hifi.fontSizes.textFieldInput
font.italic: textField.text == ""
height: implicitHeight + 3 // Make surrounding box higher so that highlight is vertically centered.
property alias textFieldLabel: textFieldLabel

View file

@ -202,8 +202,4 @@ TreeView {
}
onDoubleClicked: isExpanded(index) ? collapse(index) : expand(index)
onClicked: {
selectionModel.setCurrentIndex(index, ItemSelectionModel.ClearAndSelect);
}
}

View file

@ -0,0 +1,33 @@
module controlsUit
AttachmentsTable 1.0 AttachmentsTable.qml
BaseWebView 1.0 BaseWebView.qml
Button 1.0 Button.qml
CheckBox 1.0 CheckBox.qml
CheckBoxQQC2 1.0 CheckBoxQQC2.qml
ComboBox 1.0 ComboBox.qml
ContentSection 1.0 ContentSection.qml
GlyphButton 1.0 GlyphButton.qml
HorizontalRule 1.0 HorizontalRule.qml
HorizontalSpacer 1.0 HorizontalSpacer.qml
ImageMessageBox 1.0 ImageMessageBox.qml
Key 1.0 Key.qml
Keyboard 1.0 Keyboard.qml
Label 1.0 Label.qml
QueuedButton 1.0 QueuedButton.qml
RadioButton 1.0 RadioButton.qml
Separator 1.0 Separator.qml
Slider 1.0 Slider.qml
SpinBox 1.0 SpinBox.qml
Switch 1.0 Switch.qml
Table 1.0 Table.qml
TabletContentSection 1.0 TabletContentSection.qml
TabletHeader 1.0 TabletHeader.qml
TextAction 1.0 TextAction.qml
TextEdit 1.0 TextEdit.qml
TextField 1.0 TextField.qml
ToolTip 1.0 ToolTip.qml
Tree 1.0 Tree.qml
VerticalSpacer 1.0 VerticalSpacer.qml
WebGlyphButton 1.0 WebGlyphButton.qml
WebSpinner 1.0 WebSpinner.qml
WebView 1.0 WebView.qml

View file

@ -4,7 +4,7 @@ import QtQuick.Controls.Styles 1.3
Text {
id: root
FontLoader { id: iconFont; source: "../../fonts/fontawesome-webfont.ttf"; }
FontLoader { id: iconFont; source: "qrc:/fonts/fontawesome-webfont.ttf"; }
property int size: 32
width: size
height: size

View file

@ -532,8 +532,8 @@ ModalWindow {
itemDelegate: Item {
clip: true
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; }
FontLoader { id: firaSansSemiBold; source: "qrc:/fonts/FiraSans-SemiBold.ttf"; }
FontLoader { id: firaSansRegular; source: "qrc:/fonts/FiraSans-Regular.ttf"; }
FiraSansSemiBold {
text: getText();

View file

@ -496,8 +496,8 @@ TabletModalWindow {
itemDelegate: Item {
clip: true
//FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
//FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; }
//FontLoader { id: firaSansSemiBold; source: "qrc:/fonts/FiraSans-SemiBold.ttf"; }
//FontLoader { id: firaSansRegular; source: "qrc:/fonts/FiraSans-Regular.ttf"; }
FiraSansSemiBold {
text: getText();

View file

@ -345,8 +345,8 @@ Item {
itemDelegate: Item {
clip: true
FontLoader { id: firaSansSemiBold; source: "../../../fonts/FiraSans-SemiBold.ttf"; }
FontLoader { id: firaSansRegular; source: "../../../fonts/FiraSans-Regular.ttf"; }
FontLoader { id: firaSansSemiBold; source: "qrc:/fonts/FiraSans-SemiBold.ttf"; }
FontLoader { id: firaSansRegular; source: "qrc:/fonts/FiraSans-Regular.ttf"; }
FiraSansSemiBold {
text: styleData.value

View file

@ -37,7 +37,7 @@ Windows.ScrollingWindow {
property var assetProxyModel: Assets.proxyModel;
property var assetMappingsModel: Assets.mappingModel;
property var currentDirectory;
property var selectedItems: treeView.selection.selectedIndexes.length;
property var selectedItemCount: treeView.selection.selectedIndexes.length;
Settings {
category: "Overlay.AssetServer"
@ -75,17 +75,17 @@ Windows.ScrollingWindow {
});
}
function doDeleteFile(path) {
console.log("Deleting " + path);
function doDeleteFile(paths) {
console.log("Deleting " + paths);
Assets.deleteMappings(path, function(err) {
Assets.deleteMappings(paths, function(err) {
if (err) {
console.log("Asset browser - error deleting path: ", path, err);
console.log("Asset browser - error deleting paths: ", paths, err);
box = errorMessageBox("There was an error deleting:\n" + path + "\n" + err);
box = errorMessageBox("There was an error deleting:\n" + paths + "\n" + err);
box.selected.connect(reload);
} else {
console.log("Asset browser - finished deleting path: ", path);
console.log("Asset browser - finished deleting paths: ", paths);
reload();
}
});
@ -143,9 +143,9 @@ Windows.ScrollingWindow {
}
function canAddToWorld(path) {
var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i];
var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i];
if (selectedItems > 1) {
if (selectedItemCount > 1) {
return false;
}
@ -155,7 +155,7 @@ Windows.ScrollingWindow {
}
function canRename() {
if (treeView.selection.hasSelection && selectedItems == 1) {
if (treeView.selection.hasSelection && selectedItemCount == 1) {
return true;
} else {
return false;
@ -181,92 +181,106 @@ Windows.ScrollingWindow {
return;
}
var SHAPE_TYPE_NONE = 0;
var SHAPE_TYPE_SIMPLE_HULL = 1;
var SHAPE_TYPE_SIMPLE_COMPOUND = 2;
var SHAPE_TYPE_STATIC_MESH = 3;
var SHAPE_TYPE_BOX = 4;
var SHAPE_TYPE_SPHERE = 5;
if (defaultURL.endsWith(".jpg") || defaultURL.endsWith(".png")) {
var name = assetProxyModel.data(treeView.selection.currentIndex);
var modelURL = "https://hifi-content.s3.amazonaws.com/DomainContent/production/default-image-model.fbx";
var textures = JSON.stringify({ "tex.picture": defaultURL});
var shapeType = "box";
var dynamic = false;
var collisionless = true;
var position = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)));
var gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 0);
Entities.addModelEntity(name, modelURL, textures, shapeType, dynamic, collisionless, position, gravity);
} else {
var SHAPE_TYPE_NONE = 0;
var SHAPE_TYPE_SIMPLE_HULL = 1;
var SHAPE_TYPE_SIMPLE_COMPOUND = 2;
var SHAPE_TYPE_STATIC_MESH = 3;
var SHAPE_TYPE_BOX = 4;
var SHAPE_TYPE_SPHERE = 5;
var SHAPE_TYPES = [];
SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision";
SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model";
SHAPE_TYPES[SHAPE_TYPE_SIMPLE_COMPOUND] = "Good - Sub-meshes";
SHAPE_TYPES[SHAPE_TYPE_STATIC_MESH] = "Exact - All polygons";
SHAPE_TYPES[SHAPE_TYPE_BOX] = "Box";
SHAPE_TYPES[SHAPE_TYPE_SPHERE] = "Sphere";
var SHAPE_TYPES = [];
SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision";
SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model";
SHAPE_TYPES[SHAPE_TYPE_SIMPLE_COMPOUND] = "Good - Sub-meshes";
SHAPE_TYPES[SHAPE_TYPE_STATIC_MESH] = "Exact - All polygons";
SHAPE_TYPES[SHAPE_TYPE_BOX] = "Box";
SHAPE_TYPES[SHAPE_TYPE_SPHERE] = "Sphere";
var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH;
var DYNAMIC_DEFAULT = false;
var prompt = desktop.customInputDialog({
textInput: {
label: "Model URL",
text: defaultURL
},
comboBox: {
label: "Automatic Collisions",
index: SHAPE_TYPE_DEFAULT,
items: SHAPE_TYPES
},
checkBox: {
label: "Dynamic",
checked: DYNAMIC_DEFAULT,
disableForItems: [
SHAPE_TYPE_STATIC_MESH
],
checkStateOnDisable: false,
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors"
}
});
prompt.selected.connect(function (jsonResult) {
if (jsonResult) {
var result = JSON.parse(jsonResult);
var url = result.textInput.trim();
var shapeType;
switch (result.comboBox) {
case SHAPE_TYPE_SIMPLE_HULL:
shapeType = "simple-hull";
break;
case SHAPE_TYPE_SIMPLE_COMPOUND:
shapeType = "simple-compound";
break;
case SHAPE_TYPE_STATIC_MESH:
shapeType = "static-mesh";
break;
case SHAPE_TYPE_BOX:
shapeType = "box";
break;
case SHAPE_TYPE_SPHERE:
shapeType = "sphere";
break;
default:
shapeType = "none";
var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_SIMPLE_COMPOUND;
var DYNAMIC_DEFAULT = false;
var prompt = desktop.customInputDialog({
textInput: {
label: "Model URL",
text: defaultURL
},
comboBox: {
label: "Automatic Collisions",
index: SHAPE_TYPE_DEFAULT,
items: SHAPE_TYPES
},
checkBox: {
label: "Dynamic",
checked: DYNAMIC_DEFAULT,
disableForItems: [
SHAPE_TYPE_STATIC_MESH
],
checkStateOnDisable: false,
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors"
}
});
var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT;
if (shapeType === "static-mesh" && dynamic) {
// The prompt should prevent this case
print("Error: model cannot be both static mesh and dynamic. This should never happen.");
} else if (url) {
var name = assetProxyModel.data(treeView.selection.currentIndex);
var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)));
var gravity;
if (dynamic) {
// Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a
// different scripting engine from QTScript.
gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 10);
} else {
gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 0);
prompt.selected.connect(function (jsonResult) {
if (jsonResult) {
var result = JSON.parse(jsonResult);
var url = result.textInput.trim();
var shapeType;
var collisionless = false;
switch (result.comboBox) {
case SHAPE_TYPE_SIMPLE_HULL:
shapeType = "simple-hull";
break;
case SHAPE_TYPE_SIMPLE_COMPOUND:
shapeType = "simple-compound";
break;
case SHAPE_TYPE_STATIC_MESH:
shapeType = "static-mesh";
break;
case SHAPE_TYPE_BOX:
shapeType = "box";
break;
case SHAPE_TYPE_SPHERE:
shapeType = "sphere";
break;
default:
shapeType = "none";
collisionless = true;
}
print("Asset browser - adding asset " + url + " (" + name + ") to world.");
var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT;
if (shapeType === "static-mesh" && dynamic) {
// The prompt should prevent this case
print("Error: model cannot be both static mesh and dynamic. This should never happen.");
} else if (url) {
var name = assetProxyModel.data(treeView.selection.currentIndex);
var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)));
var gravity;
if (dynamic) {
// Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a
// different scripting engine from QTScript.
gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 10);
} else {
gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 0);
}
// Entities.addEntity doesn't work from QML, so we use this.
Entities.addModelEntity(name, url, shapeType, dynamic, addPosition, gravity);
print("Asset browser - adding asset " + url + " (" + name + ") to world.");
// Entities.addEntity doesn't work from QML, so we use this.
Entities.addModelEntity(name, url, "", shapeType, dynamic, collisionless, addPosition, gravity);
}
}
}
});
});
}
}
function copyURLToClipboard(index) {
@ -333,29 +347,28 @@ Windows.ScrollingWindow {
});
}
function deleteFile(index) {
var path = [];
var paths = [];
if (!index) {
for (var i = 0; i < selectedItems; i++) {
treeView.selection.setCurrentIndex(treeView.selection.selectedIndexes[i], 0x100);
index = treeView.selection.currentIndex;
path[i] = assetProxyModel.data(index, 0x100);
for (var i = 0; i < selectedItemCount; ++i) {
index = treeView.selection.selectedIndexes[i];
paths[i] = assetProxyModel.data(index, 0x100);
}
}
if (!path) {
if (!paths) {
return;
}
var modalMessage = "";
var items = selectedItems.toString();
var items = selectedItemCount.toString();
var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101);
var typeString = isFolder ? 'folder' : 'file';
if (selectedItems > 1) {
if (selectedItemCount > 1) {
modalMessage = "You are about to delete " + items + " items \nDo you want to continue?";
} else {
modalMessage = "You are about to delete the following " + typeString + ":\n" + path + "\nDo you want to continue?";
modalMessage = "You are about to delete the following " + typeString + ":\n" + paths + "\nDo you want to continue?";
}
var object = desktop.messageBox({
@ -367,7 +380,7 @@ Windows.ScrollingWindow {
});
object.selected.connect(function(button) {
if (button === OriginalDialogs.StandardButton.Yes) {
doDeleteFile(path);
doDeleteFile(paths);
}
});
}
@ -647,7 +660,7 @@ Windows.ScrollingWindow {
text: styleData.value
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
FontLoader { id: firaSansSemiBold; source: "qrc:/fonts/FiraSans-SemiBold.ttf"; }
font.family: firaSansSemiBold.name
font.pixelSize: hifi.fontSizes.textFieldInput
height: hifi.dimensions.tableRowHeight
@ -694,7 +707,7 @@ Windows.ScrollingWindow {
}
}
}
}
}// End_OF( itemLoader )
Rectangle {
id: treeLabelToolTip
@ -731,50 +744,59 @@ Windows.ScrollingWindow {
showTimer.stop();
treeLabelToolTip.visible = false;
}
}
}// End_OF( treeLabelToolTip )
MouseArea {
propagateComposedEvents: true
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (!HMD.active) { // Popup only displays properly on desktop
var index = treeView.indexAt(mouse.x, mouse.y);
treeView.selection.setCurrentIndex(index, 0x0002);
contextMenu.currentIndex = index;
contextMenu.popup();
if (treeView.selection.hasSelection && !HMD.active) { // Popup only displays properly on desktop
// Only display the popup if the click triggered within
// the selection.
var clickedIndex = treeView.indexAt(mouse.x, mouse.y);
var displayContextMenu = false;
for ( var i = 0; i < selectedItemCount; ++i) {
var currentSelectedIndex = treeView.selection.selectedIndexes[i];
if (clickedIndex === currentSelectedIndex) {
contextMenu.popup();
break;
}
}
}
}
}
Menu {
id: contextMenu
title: "Edit"
property var url: ""
property var currentIndex: null
MenuItem {
text: "Copy URL"
enabled: (selectedItemCount == 1)
onTriggered: {
copyURLToClipboard(contextMenu.currentIndex);
copyURLToClipboard(treeView.selection.currentIndex);
}
}
MenuItem {
text: "Rename"
enabled: (selectedItemCount == 1)
onTriggered: {
renameFile(contextMenu.currentIndex);
renameFile(treeView.selection.currentIndex);
}
}
MenuItem {
text: "Delete"
enabled: (selectedItemCount > 0)
onTriggered: {
deleteFile(contextMenu.currentIndex);
deleteFile();
}
}
}
}
}// End_OF( contextMenu )
}// End_OF( treeView )
Row {
id: infoRow
@ -787,8 +809,8 @@ Windows.ScrollingWindow {
function makeText() {
var numPendingBakes = assetMappingsModel.numPendingBakes;
if (selectedItems > 1 || numPendingBakes === 0) {
return selectedItems + " items selected";
if (selectedItemCount > 1 || numPendingBakes === 0) {
return selectedItemCount + " items selected";
} else {
return numPendingBakes + " bakes pending"
}
@ -885,7 +907,7 @@ Windows.ScrollingWindow {
"Baking compresses and optimizes files for faster network transfer and display. We recommend you bake your content to reduce initial load times for your visitors.");
}
}
}
}// End_OF( infoRow )
HifiControls.ContentSection {
id: uploadSection
@ -945,7 +967,7 @@ Windows.ScrollingWindow {
}
}
}
}
}// End_OF( uploadSection )
}
}

View file

@ -25,8 +25,8 @@ Item {
property int dialogHeight;
property int comboOptionTextSize: 16;
property int comboBodyTextSize: 16;
FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; }
FontLoader { id: ralewaySemiBold; source: "../../fonts/Raleway-SemiBold.ttf"; }
FontLoader { id: ralewayRegular; source: "qrc:/fonts/Raleway-Regular.ttf"; }
FontLoader { id: ralewaySemiBold; source: "qrc:/fonts/Raleway-SemiBold.ttf"; }
visible: false;
id: combo;
anchors.fill: parent;

View file

@ -24,8 +24,8 @@ Item {
property real headerTextMargin: -5
property real headerGlyphMargin: -15
property bool isDesktop: false
FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; }
FontLoader { id: ralewaySemiBold; source: "../../fonts/Raleway-SemiBold.ttf"; }
FontLoader { id: ralewayRegular; source: "qrc:/fonts/Raleway-Regular.ttf"; }
FontLoader { id: ralewaySemiBold; source: "qrc:/fonts/Raleway-SemiBold.ttf"; }
visible: false
id: letterbox
anchors.fill: parent

View file

@ -23,8 +23,8 @@ Item {
property real popupTextPixelSize: 16
property real headerTextMargin: -5
property real headerGlyphMargin: -15
FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; }
FontLoader { id: ralewaySemiBold; source: "../../fonts/Raleway-SemiBold.ttf"; }
FontLoader { id: ralewayRegular; source: "qrc:/fonts/Raleway-Regular.ttf"; }
FontLoader { id: ralewaySemiBold; source: "qrc:/fonts/Raleway-SemiBold.ttf"; }
visible: false
id: letterbox
anchors.fill: parent

View file

@ -177,7 +177,7 @@ Item {
anchors.right: parent.right
anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin
// Style
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
FontLoader { id: firaSansSemiBold; source: "qrc:/fonts/FiraSans-SemiBold.ttf"; }
font.family: firaSansSemiBold.name
font.pixelSize: displayNameTextPixelSize
selectionColor: hifi.colors.blueAccent

View file

@ -30,7 +30,7 @@ Rectangle {
property int myCardWidth: width - upperRightInfoContainer.width;
property int myCardHeight: 100;
property int rowHeight: 60;
property int actionButtonWidth: 55;
property int actionButtonWidth: 65;
property int locationColumnWidth: 170;
property int nearbyNameCardWidth: nearbyTable.width - (iAmAdmin ? (actionButtonWidth * 4) : (actionButtonWidth * 2)) - 4 - hifi.dimensions.scrollbarBackgroundWidth;
property int connectionsNameCardWidth: connectionsTable.width - locationColumnWidth - actionButtonWidth - 4 - hifi.dimensions.scrollbarBackgroundWidth;
@ -48,7 +48,7 @@ Rectangle {
// The letterbox used for popup messages
LetterboxMessage {
id: letterboxMessage;
z: 999; // Force the popup on top of everything else
z: 998; // Force the popup on top of everything else
}
Connections {
target: GlobalServices
@ -60,7 +60,7 @@ Rectangle {
// The ComboDialog used for setting availability
ComboDialog {
id: comboDialog;
z: 999; // Force the ComboDialog on top of everything else
z: 998; // Force the ComboDialog on top of everything else
dialogWidth: parent.width - 50;
dialogHeight: parent.height - 100;
}
@ -415,6 +415,7 @@ Rectangle {
movable: false;
resizable: false;
}
TableViewColumn {
role: "ignore";
title: "IGNORE";
@ -599,13 +600,23 @@ Rectangle {
}
// This Rectangle refers to the [?] popup button next to "NAMES"
Rectangle {
id: questionRect
color: hifi.colors.tableBackgroundLight;
width: 20;
height: hifi.dimensions.tableHeaderHeight - 2;
anchors.left: nearbyTable.left;
anchors.top: nearbyTable.top;
anchors.topMargin: 1;
anchors.leftMargin: actionButtonWidth + nearbyNameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6;
Connections {
target: nearbyTable
onTitlePaintedPosSignal: {
if (column === 1) { // name column
questionRect.anchors.leftMargin = actionButtonWidth + nearbyTable.titlePaintedPos[column]
}
}
}
RalewayRegular {
id: helpText;
text: "[?]";
@ -897,7 +908,7 @@ Rectangle {
anchors.horizontalCenter: parent.horizontalCenter;
}
FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; }
FontLoader { id: ralewayRegular; source: "qrc:/fonts/Raleway-Regular.ttf"; }
Text {
id: connectionHelpText;
// Anchors
@ -1013,7 +1024,7 @@ Rectangle {
}
MouseArea {
anchors.fill: parent;
enabled: myData.userName !== "Unknown user";
enabled: myData.userName !== "Unknown user" && !userInfoViewer.visible;
hoverEnabled: true;
onClicked: {
popupComboDialog("Set your availability:",
@ -1044,6 +1055,7 @@ Rectangle {
HifiControls.TabletWebView {
id: userInfoViewer;
z: 999;
anchors {
top: parent.top;
bottom: parent.bottom;

View file

@ -28,7 +28,7 @@ Rectangle {
id: root;
objectName: "checkout"
property string activeView: "initialize";
property bool purchasesReceived: false;
property bool ownershipStatusReceived: false;
property bool balanceReceived: false;
property string itemName;
property string itemId;
@ -37,12 +37,18 @@ Rectangle {
property double balanceAfterPurchase;
property bool alreadyOwned: false;
property int itemPrice: -1;
property bool itemIsJson: true;
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 bool shouldBuyWithControlledFailure: false;
property bool debugCheckoutSuccess: false;
property bool canRezCertifiedItems: Entities.canRezCertified() || Entities.canRezTmpCertified();
property bool isWearable;
property string referrer;
property bool isInstalled;
// Style
color: hifi.colors.white;
Connections {
@ -53,17 +59,17 @@ Rectangle {
if (root.activeView !== "needsLogIn") {
root.activeView = "needsLogIn";
}
} else if (walletStatus === 1) {
} else if ((walletStatus === 1) || (walletStatus === 2) || (walletStatus === 3)) {
if (root.activeView !== "notSetUp") {
root.activeView = "notSetUp";
notSetUpTimer.start();
}
} else if (walletStatus === 2) {
} else if (walletStatus === 4) {
if (root.activeView !== "passphraseModal") {
root.activeView = "passphraseModal";
UserActivityLogger.commercePassphraseEntry("marketplace checkout");
}
} else if (walletStatus === 3) {
} else if (walletStatus === 5) {
authSuccessStep();
} else {
console.log("ERROR in Checkout.qml: Unknown wallet status: " + walletStatus);
@ -85,7 +91,9 @@ Rectangle {
UserActivityLogger.commercePurchaseFailure(root.itemId, root.itemAuthor, root.itemPrice, !root.alreadyOwned, result.message);
} else {
root.itemHref = result.data.download_url;
root.isWearable = result.data.categories.indexOf("Wearables") > -1;
if (result.data.categories.indexOf("Wearables") > -1) {
root.itemType = "wearable";
}
root.activeView = "checkoutSuccess";
UserActivityLogger.commercePurchaseSuccess(root.itemId, root.itemAuthor, root.itemPrice, !root.alreadyOwned);
}
@ -97,32 +105,60 @@ Rectangle {
} else {
root.balanceReceived = true;
root.balanceAfterPurchase = result.data.balance - root.itemPrice;
root.setBuyText();
root.refreshBuyUI();
}
}
onInventoryResult: {
onAlreadyOwnedResult: {
if (result.status !== 'success') {
console.log("Failed to get purchases", result.data.message);
console.log("Failed to get Already Owned status", result.data.message);
} else {
root.purchasesReceived = true;
if (purchasesContains(result.data.assets, itemId)) {
root.alreadyOwned = true;
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.setBuyText();
root.refreshBuyUI();
}
}
onAppInstalled: {
if (appHref === root.itemHref) {
root.isInstalled = true;
}
}
}
onItemIdChanged: {
Commerce.inventory();
root.ownershipStatusReceived = false;
Commerce.alreadyOwned(root.itemId);
itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg";
}
onItemHrefChanged: {
itemIsJson = root.itemHref.endsWith('.json');
if (root.itemHref.indexOf(".fst") > -1) {
root.itemType = "avatar";
} else if (root.itemHref.indexOf('.json.gz') > -1) {
root.itemType = "contentSet";
} else if (root.itemHref.indexOf('.app.json') > -1) {
root.itemType = "app";
} else if (root.itemHref.indexOf('.json') > -1) {
root.itemType = "entity"; // "wearable" type handled later
} else {
console.log("WARNING - Item type is UNKNOWN!");
root.itemType = "entity";
}
}
onItemTypeChanged: {
if (root.itemType === "entity" || root.itemType === "wearable" ||
root.itemType === "contentSet" || root.itemType === "avatar" || root.itemType === "app") {
root.isCertified = true;
} else {
root.isCertified = false;
}
}
onItemPriceChanged: {
@ -203,7 +239,7 @@ Rectangle {
color: hifi.colors.white;
Component.onCompleted: {
purchasesReceived = false;
ownershipStatusReceived = false;
balanceReceived = false;
Commerce.getWalletStatus();
}
@ -278,6 +314,32 @@ Rectangle {
anchors.left: parent.left;
anchors.right: parent.right;
Rectangle {
id: loading;
z: 997;
visible: !root.ownershipStatusReceived || !root.balanceReceived;
anchors.fill: parent;
color: hifi.colors.white;
// This object is always used in a popup.
// This MouseArea is used to prevent a user from being
// able to click on a button/mouseArea underneath the popup/section.
MouseArea {
anchors.fill: parent;
hoverEnabled: true;
propagateComposedEvents: false;
}
AnimatedImage {
id: loadingImage;
source: "../common/images/loader-blue.gif"
width: 74;
height: width;
anchors.verticalCenter: parent.verticalCenter;
anchors.horizontalCenter: parent.horizontalCenter;
}
}
RalewayRegular {
id: confirmPurchaseText;
anchors.top: parent.top;
@ -286,8 +348,8 @@ Rectangle {
anchors.leftMargin: 16;
width: paintedWidth;
height: paintedHeight;
text: "Confirm Purchase:";
color: hifi.colors.baseGray;
text: "Review Purchase:";
color: hifi.colors.black;
size: 28;
}
@ -400,7 +462,7 @@ Rectangle {
width: root.width;
// Anchors
anchors.top: separator2.bottom;
anchors.topMargin: 16;
anchors.topMargin: 0;
anchors.left: parent.left;
anchors.leftMargin: 16;
anchors.right: parent.right;
@ -411,8 +473,8 @@ Rectangle {
Rectangle {
id: buyTextContainer;
visible: buyText.text !== "";
anchors.top: cancelPurchaseButton.bottom;
anchors.topMargin: 16;
anchors.top: parent.top;
anchors.topMargin: 10;
anchors.left: parent.left;
anchors.right: parent.right;
height: buyText.height + 30;
@ -454,32 +516,63 @@ Rectangle {
// Alignment
horizontalAlignment: Text.AlignLeft;
verticalAlignment: Text.AlignVCenter;
}
}
onLinkActivated: {
sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName});
}
// "View in My Purchases" button
HifiControlsUit.Button {
id: viewInMyPurchasesButton;
visible: false;
color: hifi.buttons.blue;
colorScheme: hifi.colorSchemes.light;
anchors.top: buyTextContainer.visible ? buyTextContainer.bottom : checkoutActionButtonsContainer.top;
anchors.topMargin: 10;
height: 50;
anchors.left: parent.left;
anchors.right: parent.right;
text: "VIEW THIS ITEM IN MY PURCHASES";
onClicked: {
sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName});
}
}
// "Buy" button
HifiControlsUit.Button {
id: buyButton;
enabled: (root.balanceAfterPurchase >= 0 && purchasesReceived && balanceReceived) || !itemIsJson;
color: hifi.buttons.blue;
visible: !((root.itemType === "avatar" || root.itemType === "app") && viewInMyPurchasesButton.visible)
enabled: (root.balanceAfterPurchase >= 0 && ownershipStatusReceived && balanceReceived) || (!root.isCertified);
color: viewInMyPurchasesButton.visible ? hifi.buttons.white : hifi.buttons.blue;
colorScheme: hifi.colorSchemes.light;
anchors.top: checkoutActionButtonsContainer.top;
anchors.topMargin: 16;
height: 40;
anchors.top: viewInMyPurchasesButton.visible ? viewInMyPurchasesButton.bottom :
(buyTextContainer.visible ? buyTextContainer.bottom : checkoutActionButtonsContainer.top);
anchors.topMargin: 10;
height: 50;
anchors.left: parent.left;
anchors.right: parent.right;
text: (itemIsJson ? ((purchasesReceived && balanceReceived) ? "Confirm Purchase" : "--") : "Get Item");
text: ((root.isCertified) ? ((ownershipStatusReceived && balanceReceived) ?
(viewInMyPurchasesButton.visible ? "Buy It Again" : "Confirm Purchase") : "--") : "Get Item");
onClicked: {
if (itemIsJson) {
buyButton.enabled = false;
if (root.isCertified) {
if (!root.shouldBuyWithControlledFailure) {
Commerce.buy(itemId, itemPrice);
if (root.itemType === "contentSet" && !Entities.canReplaceContent()) {
lightboxPopup.titleText = "Purchase Content Set";
lightboxPopup.bodyText = "You will not be able to replace this domain's content with <b>" + root.itemName +
" </b>until the server owner gives you 'Replace Content' permissions.<br><br>Are you sure you want to purchase this content set?";
lightboxPopup.button1text = "CANCEL";
lightboxPopup.button1method = "root.visible = false;"
lightboxPopup.button2text = "CONFIRM";
lightboxPopup.button2method = "Commerce.buy('" + root.itemId + "', " + root.itemPrice + ");" +
"root.visible = false; buyButton.enabled = false; loading.visible = true;";
lightboxPopup.visible = true;
} else {
buyButton.enabled = false;
loading.visible = true;
Commerce.buy(root.itemId, root.itemPrice);
}
} else {
Commerce.buy(itemId, itemPrice, true);
buyButton.enabled = false;
loading.visible = true;
Commerce.buy(root.itemId, root.itemPrice, true);
}
} else {
if (urlHandler.canHandleUrl(itemHref)) {
@ -494,9 +587,9 @@ Rectangle {
id: cancelPurchaseButton;
color: hifi.buttons.noneBorderlessGray;
colorScheme: hifi.colorSchemes.light;
anchors.top: buyButton.bottom;
anchors.topMargin: 16;
height: 40;
anchors.top: buyButton.visible ? buyButton.bottom : viewInMyPurchasesButton.bottom;
anchors.topMargin: 10;
height: 50;
anchors.left: parent.left;
anchors.right: parent.right;
text: "Cancel"
@ -522,31 +615,32 @@ Rectangle {
anchors.top: titleBarContainer.bottom;
anchors.bottom: root.bottom;
anchors.left: parent.left;
anchors.leftMargin: 16;
anchors.leftMargin: 20;
anchors.right: parent.right;
anchors.rightMargin: 16;
anchors.rightMargin: 20;
RalewayRegular {
id: completeText;
anchors.top: parent.top;
anchors.topMargin: 30;
anchors.topMargin: 18;
anchors.left: parent.left;
width: paintedWidth;
height: paintedHeight;
text: "Thank you for your order!";
color: hifi.colors.baseGray;
size: 28;
size: 36;
}
RalewaySemiBold {
id: completeText2;
text: "The item " + '<font color="' + hifi.colors.blueAccent + '"><a href="#">' + root.itemName + '</a></font>' +
" has been added to your Purchases and a receipt will appear in your Wallet's transaction history.";
text: "The " + (root.itemTypesText)[itemTypesArray.indexOf(root.itemType)] +
' <font color="' + hifi.colors.blueAccent + '"><a href="#">' + root.itemName + '</a></font>' +
" has been added to your Purchases and a receipt will appear in your Wallet's transaction history.";
// Text size
size: 20;
size: 18;
// Anchors
anchors.top: completeText.bottom;
anchors.topMargin: 10;
anchors.topMargin: 15;
height: paintedHeight;
anchors.left: parent.left;
anchors.right: parent.right;
@ -576,7 +670,7 @@ Rectangle {
RalewayBold {
anchors.fill: parent;
text: "REZZED";
text: (root.buttonTextClicked)[itemTypesArray.indexOf(root.itemType)];
size: 18;
color: hifi.colors.white;
verticalAlignment: Text.AlignVCenter;
@ -592,26 +686,58 @@ Rectangle {
// "Rez" button
HifiControlsUit.Button {
id: rezNowButton;
enabled: root.canRezCertifiedItems || root.isWearable;
buttonGlyph: hifi.glyphs.lightning;
enabled: (root.itemType === "entity" && root.canRezCertifiedItems) ||
(root.itemType === "contentSet" && Entities.canReplaceContent()) ||
root.itemType === "wearable" || root.itemType === "avatar" || root.itemType === "app";
buttonGlyph: (root.buttonGlyph)[itemTypesArray.indexOf(root.itemType)];
color: hifi.buttons.red;
colorScheme: hifi.colorSchemes.light;
anchors.top: completeText2.bottom;
anchors.topMargin: 30;
anchors.topMargin: 27;
height: 50;
anchors.left: parent.left;
anchors.right: parent.right;
text: root.isWearable ? "Wear It" : "Rez It"
text: root.itemType === "app" && root.isInstalled ? "OPEN APP" : (root.buttonTextNormal)[itemTypesArray.indexOf(root.itemType)];
onClicked: {
sendToScript({method: 'checkout_rezClicked', itemHref: root.itemHref, isWearable: root.isWearable});
rezzedNotifContainer.visible = true;
rezzedNotifContainerTimer.start();
UserActivityLogger.commerceEntityRezzed(root.itemId, "checkout", root.isWearable ? "rez" : "wear");
if (root.itemType === "contentSet") {
lightboxPopup.titleText = "Replace Content";
lightboxPopup.bodyText = "Rezzing this content set will replace the existing environment and all of the items in this domain. " +
"If you want to save the state of the content in this domain, create a backup before proceeding.<br><br>" +
"For more information about backing up and restoring content, " +
"<a href='https://docs.highfidelity.com/create-and-explore/start-working-in-your-sandbox/restoring-sandbox-content'>" +
"click here to open info on your desktop browser.";
lightboxPopup.button1text = "CANCEL";
lightboxPopup.button1method = "root.visible = false;"
lightboxPopup.button2text = "CONFIRM";
lightboxPopup.button2method = "Commerce.replaceContentSet('" + root.itemHref + "');" +
"root.visible = false;rezzedNotifContainer.visible = true; rezzedNotifContainerTimer.start();" +
"UserActivityLogger.commerceEntityRezzed('" + root.itemId + "', 'checkout', '" + root.itemType + "');";
lightboxPopup.visible = true;
} else if (root.itemType === "avatar") {
lightboxPopup.titleText = "Change Avatar";
lightboxPopup.bodyText = "This will change your current avatar to " + root.itemName + " while retaining your wearables.";
lightboxPopup.button1text = "CANCEL";
lightboxPopup.button1method = "root.visible = false;"
lightboxPopup.button2text = "CONFIRM";
lightboxPopup.button2method = "MyAvatar.useFullAvatarURL('" + root.itemHref + "'); root.visible = false;";
lightboxPopup.visible = true;
} else if (root.itemType === "app") {
if (root.isInstalled) {
Commerce.openApp(root.itemHref);
} else {
Commerce.installApp(root.itemHref);
}
} else {
sendToScript({method: 'checkout_rezClicked', itemHref: root.itemHref, itemType: root.itemType});
rezzedNotifContainer.visible = true;
rezzedNotifContainerTimer.start();
UserActivityLogger.commerceEntityRezzed(root.itemId, "checkout", root.itemType);
}
}
}
RalewaySemiBold {
id: noPermissionText;
visible: !root.canRezCertifiedItems && !root.isWearable;
visible: !root.canRezCertifiedItems && root.itemType === "entity";
text: '<font color="' + hifi.colors.redAccent + '"><a href="#">You do not have Certified Rez permissions in this domain.</a></font>'
// Text size
size: 16;
@ -640,7 +766,7 @@ Rectangle {
}
RalewaySemiBold {
id: explainRezText;
visible: !root.isWearable;
visible: root.itemType === "entity";
text: '<font color="' + hifi.colors.redAccent + '"><a href="#">What does "Rez" mean?</a></font>'
// Text size
size: 16;
@ -663,9 +789,9 @@ Rectangle {
RalewaySemiBold {
id: myPurchasesLink;
text: '<font color="' + hifi.colors.blueAccent + '"><a href="#">View this item in My Purchases</a></font>';
text: '<font color="' + hifi.colors.primaryHighlight + '"><a href="#">View this item in My Purchases</a></font>';
// Text size
size: 20;
size: 18;
// Anchors
anchors.top: explainRezText.visible ? explainRezText.bottom : (noPermissionText.visible ? noPermissionText.bottom : rezNowButton.bottom);
anchors.topMargin: 40;
@ -685,12 +811,12 @@ Rectangle {
RalewaySemiBold {
id: walletLink;
text: '<font color="' + hifi.colors.blueAccent + '"><a href="#">View receipt in Wallet</a></font>';
text: '<font color="' + hifi.colors.primaryHighlight + '"><a href="#">View receipt in Wallet</a></font>';
// Text size
size: 20;
size: 18;
// Anchors
anchors.top: myPurchasesLink.bottom;
anchors.topMargin: 20;
anchors.topMargin: 16;
height: paintedHeight;
anchors.left: parent.left;
anchors.right: parent.right;
@ -708,12 +834,12 @@ Rectangle {
RalewayRegular {
id: pendingText;
text: 'Your item is marked "pending" while your purchase is being confirmed. ' +
'<font color="' + hifi.colors.blueAccent + '"><a href="#">Learn More</a></font>';
'<b><font color="' + hifi.colors.primaryHighlight + '"><a href="#">Learn More</a></font></b>';
// Text size
size: 20;
size: 18;
// Anchors
anchors.top: walletLink.bottom;
anchors.topMargin: 60;
anchors.topMargin: 32;
height: paintedHeight;
anchors.left: parent.left;
anchors.right: parent.right;
@ -739,11 +865,10 @@ Rectangle {
color: hifi.buttons.noneBorderlessGray;
colorScheme: hifi.colorSchemes.light;
anchors.bottom: parent.bottom;
anchors.bottomMargin: 20;
anchors.bottomMargin: 54;
anchors.right: parent.right;
anchors.rightMargin: 14;
width: parent.width/2 - anchors.rightMargin;
height: 60;
width: 193;
height: 44;
text: "Continue Shopping";
onClicked: {
sendToScript({method: 'checkout_continueShopping', itemId: itemId});
@ -851,7 +976,7 @@ Rectangle {
buyButton.color = hifi.buttons.red;
root.shouldBuyWithControlledFailure = true;
} else {
buyButton.text = (itemIsJson ? ((purchasesReceived && balanceReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item");
buyButton.text = (root.isCertified ? ((ownershipStatusReceived && balanceReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item");
buyButton.color = hifi.buttons.blue;
root.shouldBuyWithControlledFailure = false;
}
@ -883,7 +1008,7 @@ Rectangle {
itemHref = message.params.itemHref;
referrer = message.params.referrer;
itemAuthor = message.params.itemAuthor;
setBuyText();
refreshBuyUI();
break;
default:
console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message));
@ -891,22 +1016,13 @@ Rectangle {
}
signal sendToScript(var message);
function purchasesContains(purchasesJson, id) {
for (var idx = 0; idx < purchasesJson.length; idx++) {
if(purchasesJson[idx].id === id) {
return true;
}
}
return false;
}
function setBuyText() {
if (root.itemIsJson) {
if (root.purchasesReceived && root.balanceReceived) {
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.<br>" +
'<font color="' + hifi.colors.blueAccent + '"><a href="#">View the copy you own in My Purchases</a></font></b>';
buyText.text = "<b>Your Wallet does not have sufficient funds to purchase this item again.</b>";
viewInMyPurchasesButton.visible = true;
} else {
buyText.text = "<b>Your Wallet does not have sufficient funds to purchase this item.</b>";
}
@ -916,15 +1032,19 @@ Rectangle {
buyGlyph.size = 54;
} else {
if (root.alreadyOwned) {
buyText.text = '<b>You already own this item.<br>Purchasing it will buy another copy.<br><font color="'
+ hifi.colors.blueAccent + '"><a href="#">View this item in My Purchases</a></font></b>';
buyTextContainer.color = "#FFD6AD";
buyTextContainer.border.color = "#FAC07D";
buyGlyph.text = hifi.glyphs.alert;
buyGlyph.size = 46;
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;
}
}
} else {
buyText.text = "";
@ -945,8 +1065,8 @@ Rectangle {
root.activeView = "checkoutSuccess";
}
root.balanceReceived = false;
root.purchasesReceived = false;
Commerce.inventory();
root.ownershipStatusReceived = false;
Commerce.alreadyOwned(root.itemId);
Commerce.balance();
}

View file

@ -25,10 +25,13 @@ Rectangle {
property string titleText;
property string bodyImageSource;
property string bodyText;
property string button1color: hifi.buttons.noneBorderlessGray;
property string button1text;
property string button1method;
property string button2color: hifi.buttons.noneBorderless;
property string button2text;
property string button2method;
property string buttonLayout: "leftright";
readonly property string securityPicBodyText: "When you see your Security Pic, your actions and data are securely making use of your " +
"Wallet's private keys.<br><br>You can change your Security Pic in your Wallet.";
@ -39,6 +42,12 @@ Rectangle {
color: Qt.rgba(0, 0, 0, 0.5);
z: 999;
onVisibleChanged: {
if (!visible) {
resetLightbox();
}
}
// This object is always used in a popup.
// This MouseArea is used to prevent a user from being
// able to click on a button/mouseArea underneath the popup.
@ -100,6 +109,10 @@ Rectangle {
size: 20;
verticalAlignment: Text.AlignTop;
wrapMode: Text.WordWrap;
onLinkActivated: {
sendToParent({ method: 'commerceLightboxLinkClicked', linkUrl: link });
}
}
Item {
@ -108,18 +121,21 @@ Rectangle {
anchors.topMargin: 30;
anchors.left: parent.left;
anchors.right: parent.right;
height: 70;
height: root.buttonLayout === "leftright" ? 70 : 150;
// Button 1
HifiControlsUit.Button {
color: hifi.buttons.noneBorderlessGray;
id: button1;
color: root.button1color;
colorScheme: hifi.colorSchemes.light;
anchors.top: parent.top;
anchors.bottom: parent.bottom;
anchors.bottomMargin: 20;
anchors.top: root.buttonLayout === "leftright" ? parent.top : parent.top;
anchors.left: parent.left;
anchors.leftMargin: 10;
width: root.button2text ? parent.width/2 - anchors.leftMargin*2 : parent.width - anchors.leftMargin * 2;
anchors.right: root.buttonLayout === "leftright" ? undefined : parent.right;
anchors.rightMargin: root.buttonLayout === "leftright" ? undefined : 10;
width: root.buttonLayout === "leftright" ? (root.button2text ? parent.width/2 - anchors.leftMargin*2 : parent.width - anchors.leftMargin * 2) :
(undefined);
height: 50;
text: root.button1text;
onClicked: {
eval(button1method);
@ -128,15 +144,18 @@ Rectangle {
// Button 2
HifiControlsUit.Button {
id: button2;
visible: root.button2text;
color: hifi.buttons.noneBorderless;
color: root.button2color;
colorScheme: hifi.colorSchemes.light;
anchors.top: parent.top;
anchors.bottom: parent.bottom;
anchors.bottomMargin: 20;
anchors.top: root.buttonLayout === "leftright" ? parent.top : button1.bottom;
anchors.topMargin: root.buttonLayout === "leftright" ? undefined : 20;
anchors.left: root.buttonLayout === "leftright" ? undefined : parent.left;
anchors.leftMargin: root.buttonLayout === "leftright" ? undefined : 10;
anchors.right: parent.right;
anchors.rightMargin: 10;
width: parent.width/2 - anchors.rightMargin*2;
width: root.buttonLayout === "leftright" ? parent.width/2 - anchors.rightMargin*2 : undefined;
height: 50;
text: root.button2text;
onClicked: {
eval(button2method);
@ -149,6 +168,19 @@ Rectangle {
// FUNCTION DEFINITIONS START
//
signal sendToParent(var msg);
function resetLightbox() {
root.titleText = "";
root.bodyImageSource = "";
root.bodyText = "";
root.button1color = hifi.buttons.noneBorderlessGray;
root.button1text = "";
root.button1method = "";
root.button2color = hifi.buttons.noneBorderless;
root.button2text = "";
root.button2method = "";
root.buttonLayout = "leftright";
}
//
// FUNCTION DEFINITIONS END
//

View file

@ -37,9 +37,9 @@ Item {
onWalletStatusResult: {
if (walletStatus === 0) {
sendToParent({method: "needsLogIn"});
} else if (walletStatus === 3) {
} else if (walletStatus === 5) {
Commerce.getSecurityImage();
} else if (walletStatus > 3) {
} else if (walletStatus > 5) {
console.log("ERROR in EmulatedMarketplaceHeader.qml: Unknown wallet status: " + walletStatus);
}
}
@ -141,7 +141,7 @@ Item {
}
}
FontLoader { id: ralewayRegular; source: "../../../../fonts/Raleway-Regular.ttf"; }
FontLoader { id: ralewayRegular; source: "qrc:/fonts/Raleway-Regular.ttf"; }
TextMetrics {
id: textMetrics;
font.family: ralewayRegular.name

Some files were not shown because too many files have changed in this diff Show more