mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-25 17:14:59 +02:00
Merge branch 'master' of github.com:highfidelity/hifi into fix-avatar-entities-delete
This commit is contained in:
commit
5d085798cb
26 changed files with 426 additions and 138 deletions
Binary file not shown.
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 246 KiB |
|
@ -87,6 +87,8 @@ ModalWindow {
|
|||
|
||||
if (selectDirectory) {
|
||||
currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder));
|
||||
d.currentSelectionIsFolder = true;
|
||||
d.currentSelectionUrl = initialFolder;
|
||||
}
|
||||
|
||||
helper.contentsChanged.connect(function() {
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
#include <RenderShadowTask.h>
|
||||
#include <RenderDeferredTask.h>
|
||||
#include <ResourceCache.h>
|
||||
#include <SandboxUtils.h>
|
||||
#include <SceneScriptingInterface.h>
|
||||
#include <ScriptEngines.h>
|
||||
#include <ScriptCache.h>
|
||||
|
@ -415,8 +416,6 @@ bool setupEssentials(int& argc, char** argv) {
|
|||
static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset";
|
||||
bool suppressPrompt = cmdOptionExists(argc, const_cast<const char**>(argv), SUPPRESS_SETTINGS_RESET);
|
||||
bool previousSessionCrashed = CrashHandler::checkForResetSettings(suppressPrompt);
|
||||
CrashHandler::writeRunningMarkerFiler();
|
||||
qAddPostRoutine(CrashHandler::deleteRunningMarkerFile);
|
||||
|
||||
DependencyManager::registerInheritance<LimitedNodeList, NodeList>();
|
||||
DependencyManager::registerInheritance<AvatarHashMap, AvatarManager>();
|
||||
|
@ -504,8 +503,11 @@ Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context);
|
|||
|
||||
Setting::Handle<int> sessionRunTime{ "sessionRunTime", 0 };
|
||||
|
||||
Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
||||
Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) :
|
||||
QApplication(argc, argv),
|
||||
_shouldRunServer(runServer),
|
||||
_runServerPath(runServerPathOption),
|
||||
_runningMarker(this, RUNNING_MARKER_FILENAME),
|
||||
_window(new MainWindow(desktop())),
|
||||
_sessionRunTimer(startupTimer),
|
||||
_previousSessionCrashed(setupEssentials(argc, argv)),
|
||||
|
@ -529,7 +531,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
|||
_maxOctreePPS(maxOctreePacketsPerSecond.get()),
|
||||
_lastFaceTrackerUpdate(0)
|
||||
{
|
||||
|
||||
_runningMarker.startRunningMarker();
|
||||
|
||||
PluginContainer* pluginContainer = dynamic_cast<PluginContainer*>(this); // set the container for any plugins that care
|
||||
PluginManager::getInstance()->setContainer(pluginContainer);
|
||||
|
@ -575,6 +577,28 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
|||
qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services.";
|
||||
#endif
|
||||
|
||||
|
||||
bool wantsSandboxRunning = shouldRunServer();
|
||||
static bool determinedSandboxState = false;
|
||||
SandboxUtils sandboxUtils;
|
||||
sandboxUtils.ifLocalSandboxRunningElse([&]() {
|
||||
qCDebug(interfaceapp) << "Home sandbox appears to be running.....";
|
||||
determinedSandboxState = true;
|
||||
}, [&, wantsSandboxRunning]() {
|
||||
qCDebug(interfaceapp) << "Home sandbox does not appear to be running....";
|
||||
determinedSandboxState = true;
|
||||
if (wantsSandboxRunning) {
|
||||
QString contentPath = getRunServerPath();
|
||||
SandboxUtils::runLocalSandbox(contentPath, true, RUNNING_MARKER_FILENAME);
|
||||
}
|
||||
});
|
||||
|
||||
quint64 MAX_WAIT_TIME = USECS_PER_SECOND * 4;
|
||||
auto startWaiting = usecTimestampNow();
|
||||
while (!determinedSandboxState && (usecTimestampNow() - startWaiting <= MAX_WAIT_TIME)) {
|
||||
QCoreApplication::processEvents();
|
||||
usleep(USECS_PER_MSEC * 50); // 20hz
|
||||
}
|
||||
|
||||
_bookmarks = new Bookmarks(); // Before setting up the menu
|
||||
|
||||
|
@ -1265,7 +1289,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
|||
// Get sandbox content set version, if available
|
||||
auto acDirPath = PathUtils::getRootDataDirectory() + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/";
|
||||
auto contentVersionPath = acDirPath + "content-version.txt";
|
||||
qDebug() << "Checking " << contentVersionPath << " for content version";
|
||||
qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version";
|
||||
auto contentVersion = 0;
|
||||
QFile contentVersionFile(contentVersionPath);
|
||||
if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
|
@ -1273,7 +1297,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
|||
// toInt() returns 0 if the conversion fails, so we don't need to specifically check for failure
|
||||
contentVersion = line.toInt();
|
||||
}
|
||||
qDebug() << "Server content version: " << contentVersion;
|
||||
qCDebug(interfaceapp) << "Server content version: " << contentVersion;
|
||||
|
||||
bool hasTutorialContent = contentVersion >= 1;
|
||||
|
||||
|
@ -1283,10 +1307,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
|||
|
||||
bool shouldGoToTutorial = hasHMDAndHandControllers && hasTutorialContent && !tutorialComplete.get();
|
||||
|
||||
qDebug() << "Has HMD + Hand Controllers: " << hasHMDAndHandControllers << ", current plugin: " << _displayPlugin->getName();
|
||||
qDebug() << "Has tutorial content: " << hasTutorialContent;
|
||||
qDebug() << "Tutorial complete: " << tutorialComplete.get();
|
||||
qDebug() << "Should go to tutorial: " << shouldGoToTutorial;
|
||||
qCDebug(interfaceapp) << "Has HMD + Hand Controllers: " << hasHMDAndHandControllers << ", current plugin: " << _displayPlugin->getName();
|
||||
qCDebug(interfaceapp) << "Has tutorial content: " << hasTutorialContent;
|
||||
qCDebug(interfaceapp) << "Tutorial complete: " << tutorialComplete.get();
|
||||
qCDebug(interfaceapp) << "Should go to tutorial: " << shouldGoToTutorial;
|
||||
|
||||
// when --url in command line, teleport to location
|
||||
const QString HIFI_URL_COMMAND_LINE_KEY = "--url";
|
||||
|
@ -1299,11 +1323,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
|||
const QString TUTORIAL_PATH = "/tutorial_begin";
|
||||
|
||||
if (shouldGoToTutorial) {
|
||||
DependencyManager::get<AddressManager>()->ifLocalSandboxRunningElse([=]() {
|
||||
qDebug() << "Home sandbox appears to be running, going to Home.";
|
||||
sandboxUtils.ifLocalSandboxRunningElse([=]() {
|
||||
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
|
||||
DependencyManager::get<AddressManager>()->goToLocalSandbox(TUTORIAL_PATH);
|
||||
}, [=]() {
|
||||
qDebug() << "Home sandbox does not appear to be running, going to Entry.";
|
||||
qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry.";
|
||||
if (firstRun.get()) {
|
||||
showHelp();
|
||||
}
|
||||
|
@ -1324,18 +1348,18 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
|||
// If this is a first run we short-circuit the address passed in
|
||||
if (isFirstRun) {
|
||||
if (hasHMDAndHandControllers) {
|
||||
DependencyManager::get<AddressManager>()->ifLocalSandboxRunningElse([=]() {
|
||||
qDebug() << "Home sandbox appears to be running, going to Home.";
|
||||
sandboxUtils.ifLocalSandboxRunningElse([=]() {
|
||||
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
|
||||
DependencyManager::get<AddressManager>()->goToLocalSandbox();
|
||||
}, [=]() {
|
||||
qDebug() << "Home sandbox does not appear to be running, going to Entry.";
|
||||
qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry.";
|
||||
DependencyManager::get<AddressManager>()->goToEntry();
|
||||
});
|
||||
} else {
|
||||
DependencyManager::get<AddressManager>()->goToEntry();
|
||||
}
|
||||
} else {
|
||||
qDebug() << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString);
|
||||
qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString);
|
||||
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,8 @@
|
|||
#include <ThreadSafeValueCache.h>
|
||||
#include <shared/FileLogger.h>
|
||||
|
||||
#include <RunningMarker.h>
|
||||
|
||||
#include "avatar/MyAvatar.h"
|
||||
#include "Bookmarks.h"
|
||||
#include "Camera.h"
|
||||
|
@ -87,6 +89,8 @@ static const UINT UWM_SHOW_APPLICATION =
|
|||
RegisterWindowMessage("UWM_SHOW_APPLICATION_{71123FD6-3DA8-4DC1-9C27-8A12A6250CBA}_" + qgetenv("USERNAME"));
|
||||
#endif
|
||||
|
||||
static const QString RUNNING_MARKER_FILENAME = "Interface.running";
|
||||
|
||||
class Application;
|
||||
#if defined(qApp)
|
||||
#undef qApp
|
||||
|
@ -103,7 +107,16 @@ class Application : public QApplication,
|
|||
// TODO? Get rid of those
|
||||
friend class OctreePacketProcessor;
|
||||
|
||||
private:
|
||||
bool _shouldRunServer { false };
|
||||
QString _runServerPath;
|
||||
RunningMarker _runningMarker;
|
||||
|
||||
public:
|
||||
// startup related getter/setters
|
||||
bool shouldRunServer() const { return _shouldRunServer; }
|
||||
bool hasRunServerPath() const { return !_runServerPath.isEmpty(); }
|
||||
QString getRunServerPath() const { return _runServerPath; }
|
||||
|
||||
// virtual functions required for PluginContainer
|
||||
virtual ui::Menu* getPrimaryMenu() override;
|
||||
|
@ -127,7 +140,7 @@ public:
|
|||
static void initPlugins(const QStringList& arguments);
|
||||
static void shutdownPlugins();
|
||||
|
||||
Application(int& argc, char** argv, QElapsedTimer& startup_time);
|
||||
Application(int& argc, char** argv, QElapsedTimer& startup_time, bool runServer, QString runServerPathOption);
|
||||
~Application();
|
||||
|
||||
void postLambdaEvent(std::function<void()> f) override;
|
||||
|
|
|
@ -34,7 +34,7 @@ void ConnectionMonitor::init() {
|
|||
}
|
||||
|
||||
auto dialogsManager = DependencyManager::get<DialogsManager>();
|
||||
connect(&_timer, &QTimer::timeout, dialogsManager.data(), &DialogsManager::showAddressBar);
|
||||
connect(&_timer, &QTimer::timeout, dialogsManager.data(), &DialogsManager::indicateDomainConnectionFailure);
|
||||
}
|
||||
|
||||
void ConnectionMonitor::disconnectedFromDomain() {
|
||||
|
|
|
@ -23,9 +23,10 @@
|
|||
#include <QVBoxLayout>
|
||||
#include <QtCore/QUrl>
|
||||
|
||||
#include "Application.h"
|
||||
#include "Menu.h"
|
||||
|
||||
static const QString RUNNING_MARKER_FILENAME = "Interface.running";
|
||||
#include <RunningMarker.h>
|
||||
|
||||
bool CrashHandler::checkForResetSettings(bool suppressPrompt) {
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
|
@ -39,7 +40,7 @@ bool CrashHandler::checkForResetSettings(bool suppressPrompt) {
|
|||
// If option does not exist in Interface.ini so assume default behavior.
|
||||
bool displaySettingsResetOnCrash = !displayCrashOptions.isValid() || displayCrashOptions.toBool();
|
||||
|
||||
QFile runningMarkerFile(runningMarkerFilePath());
|
||||
QFile runningMarkerFile(RunningMarker::getMarkerFilePath(RUNNING_MARKER_FILENAME));
|
||||
bool wasLikelyCrash = runningMarkerFile.exists();
|
||||
|
||||
if (suppressPrompt) {
|
||||
|
@ -161,20 +162,3 @@ void CrashHandler::handleCrash(CrashHandler::Action action) {
|
|||
}
|
||||
}
|
||||
|
||||
void CrashHandler::writeRunningMarkerFiler() {
|
||||
QFile runningMarkerFile(runningMarkerFilePath());
|
||||
if (!runningMarkerFile.exists()) {
|
||||
runningMarkerFile.open(QIODevice::WriteOnly);
|
||||
runningMarkerFile.close();
|
||||
}
|
||||
}
|
||||
void CrashHandler::deleteRunningMarkerFile() {
|
||||
QFile runningMarkerFile(runningMarkerFilePath());
|
||||
if (runningMarkerFile.exists()) {
|
||||
runningMarkerFile.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const QString CrashHandler::runningMarkerFilePath() {
|
||||
return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + RUNNING_MARKER_FILENAME;
|
||||
}
|
||||
|
|
|
@ -19,9 +19,6 @@ class CrashHandler {
|
|||
public:
|
||||
static bool checkForResetSettings(bool suppressPrompt = false);
|
||||
|
||||
static void writeRunningMarkerFiler();
|
||||
static void deleteRunningMarkerFile();
|
||||
|
||||
private:
|
||||
enum Action {
|
||||
DELETE_INTERFACE_INI,
|
||||
|
@ -31,8 +28,6 @@ private:
|
|||
|
||||
static Action promptUserForAction(bool showCrashMessage);
|
||||
static void handleCrash(Action action);
|
||||
|
||||
static const QString runningMarkerFilePath();
|
||||
};
|
||||
|
||||
#endif // hifi_CrashHandler_h
|
||||
|
|
|
@ -37,6 +37,8 @@
|
|||
#include <CrashReporter.h>
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
int main(int argc, const char* argv[]) {
|
||||
#if HAS_BUGSPLAT
|
||||
static QString BUG_SPLAT_DATABASE = "interface_alpha";
|
||||
|
@ -128,22 +130,9 @@ int main(int argc, const char* argv[]) {
|
|||
parser.addOption(runServerOption);
|
||||
parser.addOption(serverContentPathOption);
|
||||
parser.parse(arguments);
|
||||
if (parser.isSet(runServerOption)) {
|
||||
QString applicationDirPath = QFileInfo(arguments[0]).path();
|
||||
QString serverPath = applicationDirPath + "/server-console/server-console.exe";
|
||||
qDebug() << "Application dir path is: " << applicationDirPath;
|
||||
qDebug() << "Server path is: " << serverPath;
|
||||
QStringList args;
|
||||
if (parser.isSet(serverContentPathOption)) {
|
||||
QString serverContentPath = QFileInfo(arguments[0]).path() + "/" + parser.value(serverContentPathOption);
|
||||
args << "--" << "--contentPath" << serverContentPath;
|
||||
}
|
||||
qDebug() << QFileInfo(arguments[0]).path();
|
||||
qDebug() << QProcess::startDetached(serverPath, args);
|
||||
|
||||
// Sleep a short amount of time to give the server a chance to start
|
||||
usleep(2000000);
|
||||
}
|
||||
bool runServer = parser.isSet(runServerOption);
|
||||
bool serverContentPathOptionIsSet = parser.isSet(serverContentPathOption);
|
||||
QString serverContentPathOptionValue = serverContentPathOptionIsSet ? parser.value(serverContentPathOption) : QString();
|
||||
|
||||
QElapsedTimer startupTime;
|
||||
startupTime.start();
|
||||
|
@ -166,10 +155,11 @@ int main(int argc, const char* argv[]) {
|
|||
|
||||
SteamClient::init();
|
||||
|
||||
|
||||
int exitCode;
|
||||
{
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
Application app(argc, const_cast<char**>(argv), startupTime);
|
||||
Application app(argc, const_cast<char**>(argv), startupTime, runServer, serverContentPathOptionValue);
|
||||
|
||||
// If we failed the OpenGLVersion check, log it.
|
||||
if (override) {
|
||||
|
@ -223,7 +213,6 @@ int main(int argc, const char* argv[]) {
|
|||
QTranslator translator;
|
||||
translator.load("i18n/interface_en");
|
||||
app.installTranslator(&translator);
|
||||
|
||||
qCDebug(interfaceapp, "Created QT Application.");
|
||||
exitCode = app.exec();
|
||||
server.close();
|
||||
|
|
|
@ -59,6 +59,10 @@ void DialogsManager::showFeed() {
|
|||
emit setUseFeed(true);
|
||||
}
|
||||
|
||||
void DialogsManager::indicateDomainConnectionFailure() {
|
||||
OffscreenUi::information("No Connection", "Unable to connect to this domain. Click the 'GO TO' button on the toolbar to visit another domain.");
|
||||
}
|
||||
|
||||
void DialogsManager::toggleDiskCacheEditor() {
|
||||
maybeCreateDialog(_diskCacheEditor);
|
||||
_diskCacheEditor->toggle();
|
||||
|
|
|
@ -46,6 +46,7 @@ public slots:
|
|||
void toggleAddressBar();
|
||||
void showAddressBar();
|
||||
void showFeed();
|
||||
void indicateDomainConnectionFailure();
|
||||
void toggleDiskCacheEditor();
|
||||
void toggleLoginDialog();
|
||||
void showLoginDialog();
|
||||
|
|
|
@ -43,9 +43,7 @@ const QString SNAPSHOTS_DIRECTORY = "Snapshots";
|
|||
|
||||
const QString URL = "highfidelity_url";
|
||||
|
||||
Setting::Handle<QString> Snapshot::snapshotsLocation("snapshotsLocation",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DesktopLocation));
|
||||
Setting::Handle<bool> Snapshot::hasSetSnapshotsLocation("hasSetSnapshotsLocation", false);
|
||||
Setting::Handle<QString> Snapshot::snapshotsLocation("snapshotsLocation");
|
||||
|
||||
SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) {
|
||||
|
||||
|
@ -105,42 +103,44 @@ QFile* Snapshot::savedFileForSnapshot(QImage & shot, bool isTemporary) {
|
|||
const int IMAGE_QUALITY = 100;
|
||||
|
||||
if (!isTemporary) {
|
||||
QString snapshotFullPath;
|
||||
if (!hasSetSnapshotsLocation.get()) {
|
||||
snapshotFullPath = QFileDialog::getExistingDirectory(nullptr, "Choose Snapshots Directory", snapshotsLocation.get());
|
||||
hasSetSnapshotsLocation.set(true);
|
||||
QString snapshotFullPath = snapshotsLocation.get();
|
||||
|
||||
if (snapshotFullPath.isEmpty()) {
|
||||
snapshotFullPath = OffscreenUi::getExistingDirectory(nullptr, "Choose Snapshots Directory", QStandardPaths::writableLocation(QStandardPaths::DesktopLocation));
|
||||
snapshotsLocation.set(snapshotFullPath);
|
||||
} else {
|
||||
snapshotFullPath = snapshotsLocation.get();
|
||||
}
|
||||
|
||||
if (!snapshotFullPath.endsWith(QDir::separator())) {
|
||||
snapshotFullPath.append(QDir::separator());
|
||||
if (!snapshotFullPath.isEmpty()) { // not cancelled
|
||||
|
||||
if (!snapshotFullPath.endsWith(QDir::separator())) {
|
||||
snapshotFullPath.append(QDir::separator());
|
||||
}
|
||||
|
||||
snapshotFullPath.append(filename);
|
||||
|
||||
QFile* imageFile = new QFile(snapshotFullPath);
|
||||
imageFile->open(QIODevice::WriteOnly);
|
||||
|
||||
shot.save(imageFile, 0, IMAGE_QUALITY);
|
||||
imageFile->close();
|
||||
|
||||
return imageFile;
|
||||
}
|
||||
|
||||
snapshotFullPath.append(filename);
|
||||
|
||||
QFile* imageFile = new QFile(snapshotFullPath);
|
||||
imageFile->open(QIODevice::WriteOnly);
|
||||
|
||||
shot.save(imageFile, 0, IMAGE_QUALITY);
|
||||
imageFile->close();
|
||||
|
||||
return imageFile;
|
||||
|
||||
} else {
|
||||
QTemporaryFile* imageTempFile = new QTemporaryFile(QDir::tempPath() + "/XXXXXX-" + filename);
|
||||
|
||||
if (!imageTempFile->open()) {
|
||||
qDebug() << "Unable to open QTemporaryFile for temp snapshot. Will not save.";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
shot.save(imageTempFile, 0, IMAGE_QUALITY);
|
||||
imageTempFile->close();
|
||||
|
||||
return imageTempFile;
|
||||
}
|
||||
// Either we were asked for a tempororary, or the user didn't set a directory.
|
||||
QTemporaryFile* imageTempFile = new QTemporaryFile(QDir::tempPath() + "/XXXXXX-" + filename);
|
||||
|
||||
if (!imageTempFile->open()) {
|
||||
qDebug() << "Unable to open QTemporaryFile for temp snapshot. Will not save.";
|
||||
return NULL;
|
||||
}
|
||||
imageTempFile->setAutoRemove(isTemporary);
|
||||
|
||||
shot.save(imageTempFile, 0, IMAGE_QUALITY);
|
||||
imageTempFile->close();
|
||||
|
||||
return imageTempFile;
|
||||
}
|
||||
|
||||
void Snapshot::uploadSnapshot(const QString& filename) {
|
||||
|
|
|
@ -39,7 +39,6 @@ public:
|
|||
static SnapshotMetaData* parseSnapshotData(QString snapshotPath);
|
||||
|
||||
static Setting::Handle<QString> snapshotsLocation;
|
||||
static Setting::Handle<bool> hasSetSnapshotsLocation;
|
||||
static void uploadSnapshot(const QString& filename);
|
||||
private:
|
||||
static QFile* savedFileForSnapshot(QImage & image, bool isTemporary);
|
||||
|
|
|
@ -831,32 +831,3 @@ void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) {
|
|||
}
|
||||
}
|
||||
|
||||
void AddressManager::ifLocalSandboxRunningElse(std::function<void()> localSandboxRunningDoThis,
|
||||
std::function<void()> localSandboxNotRunningDoThat) {
|
||||
|
||||
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
|
||||
QNetworkRequest sandboxStatus(SANDBOX_STATUS_URL);
|
||||
sandboxStatus.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
sandboxStatus.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
|
||||
QNetworkReply* reply = networkAccessManager.get(sandboxStatus);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, localSandboxRunningDoThis, localSandboxNotRunningDoThat]() {
|
||||
auto statusData = reply->readAll();
|
||||
auto statusJson = QJsonDocument::fromJson(statusData);
|
||||
if (!statusJson.isEmpty()) {
|
||||
auto statusObject = statusJson.object();
|
||||
auto serversValue = statusObject.value("servers");
|
||||
if (!serversValue.isUndefined() && serversValue.isObject()) {
|
||||
auto serversObject = serversValue.toObject();
|
||||
auto serversCount = serversObject.size();
|
||||
const int MINIMUM_EXPECTED_SERVER_COUNT = 5;
|
||||
if (serversCount >= MINIMUM_EXPECTED_SERVER_COUNT) {
|
||||
localSandboxRunningDoThis();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
localSandboxNotRunningDoThat();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
const QString HIFI_URL_SCHEME = "hifi";
|
||||
const QString DEFAULT_HIFI_ADDRESS = "hifi://welcome";
|
||||
const QString SANDBOX_HIFI_ADDRESS = "hifi://localhost";
|
||||
const QString SANDBOX_STATUS_URL = "http://localhost:60332/status";
|
||||
const QString INDEX_PATH = "/";
|
||||
|
||||
const QString GET_PLACE = "/api/v1/places/%1";
|
||||
|
@ -78,11 +77,6 @@ public:
|
|||
const QStack<QUrl>& getBackStack() const { return _backStack; }
|
||||
const QStack<QUrl>& getForwardStack() const { return _forwardStack; }
|
||||
|
||||
/// determines if the local sandbox is likely running. It does not account for custom setups, and is only
|
||||
/// intended to detect the standard local sandbox install.
|
||||
void ifLocalSandboxRunningElse(std::function<void()> localSandboxRunningDoThis,
|
||||
std::function<void()> localSandboxNotRunningDoThat);
|
||||
|
||||
public slots:
|
||||
void handleLookupString(const QString& lookupString, bool fromSuggestions = false);
|
||||
|
||||
|
|
96
libraries/networking/src/SandboxUtils.cpp
Normal file
96
libraries/networking/src/SandboxUtils.cpp
Normal file
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// SandboxUtils.cpp
|
||||
// libraries/networking/src
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 2016-10-15.
|
||||
// Copyright 2016 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 <QDataStream>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
|
||||
#include <NumericalConstants.h>
|
||||
#include <SharedUtil.h>
|
||||
#include <RunningMarker.h>
|
||||
|
||||
#include "SandboxUtils.h"
|
||||
#include "NetworkAccessManager.h"
|
||||
|
||||
|
||||
void SandboxUtils::ifLocalSandboxRunningElse(std::function<void()> localSandboxRunningDoThis,
|
||||
std::function<void()> localSandboxNotRunningDoThat) {
|
||||
|
||||
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
|
||||
QNetworkRequest sandboxStatus(SANDBOX_STATUS_URL);
|
||||
sandboxStatus.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
sandboxStatus.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
|
||||
QNetworkReply* reply = networkAccessManager.get(sandboxStatus);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, localSandboxRunningDoThis, localSandboxNotRunningDoThat]() {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
auto statusData = reply->readAll();
|
||||
auto statusJson = QJsonDocument::fromJson(statusData);
|
||||
if (!statusJson.isEmpty()) {
|
||||
auto statusObject = statusJson.object();
|
||||
auto serversValue = statusObject.value("servers");
|
||||
if (!serversValue.isUndefined() && serversValue.isObject()) {
|
||||
auto serversObject = serversValue.toObject();
|
||||
auto serversCount = serversObject.size();
|
||||
const int MINIMUM_EXPECTED_SERVER_COUNT = 5;
|
||||
if (serversCount >= MINIMUM_EXPECTED_SERVER_COUNT) {
|
||||
localSandboxRunningDoThis();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
localSandboxNotRunningDoThat();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void SandboxUtils::runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName) {
|
||||
QString applicationDirPath = QFileInfo(QCoreApplication::applicationFilePath()).path();
|
||||
QString serverPath = applicationDirPath + "/server-console/server-console.exe";
|
||||
qDebug() << "Application dir path is: " << applicationDirPath;
|
||||
qDebug() << "Server path is: " << serverPath;
|
||||
qDebug() << "autoShutdown: " << autoShutdown;
|
||||
|
||||
bool hasContentPath = !contentPath.isEmpty();
|
||||
bool passArgs = autoShutdown || hasContentPath;
|
||||
|
||||
QStringList args;
|
||||
|
||||
if (passArgs) {
|
||||
args << "--";
|
||||
}
|
||||
|
||||
if (hasContentPath) {
|
||||
QString serverContentPath = applicationDirPath + "/" + contentPath;
|
||||
args << "--contentPath" << serverContentPath;
|
||||
}
|
||||
|
||||
if (autoShutdown) {
|
||||
QString interfaceRunningStateFile = RunningMarker::getMarkerFilePath(runningMarkerName);
|
||||
args << "--shutdownWatcher" << interfaceRunningStateFile;
|
||||
}
|
||||
|
||||
qDebug() << applicationDirPath;
|
||||
qDebug() << "Launching sandbox with:" << args;
|
||||
qDebug() << QProcess::startDetached(serverPath, args);
|
||||
|
||||
// Sleep a short amount of time to give the server a chance to start
|
||||
usleep(2000000); /// do we really need this??
|
||||
}
|
32
libraries/networking/src/SandboxUtils.h
Normal file
32
libraries/networking/src/SandboxUtils.h
Normal file
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// SandboxUtils.h
|
||||
// libraries/networking/src
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 2016-10-15.
|
||||
// Copyright 2016 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_SandboxUtils_h
|
||||
#define hifi_SandboxUtils_h
|
||||
|
||||
#include <functional>
|
||||
#include <QtCore/QObject>
|
||||
|
||||
|
||||
const QString SANDBOX_STATUS_URL = "http://localhost:60332/status";
|
||||
|
||||
class SandboxUtils : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
/// determines if the local sandbox is likely running. It does not account for custom setups, and is only
|
||||
/// intended to detect the standard local sandbox install.
|
||||
void ifLocalSandboxRunningElse(std::function<void()> localSandboxRunningDoThis,
|
||||
std::function<void()> localSandboxNotRunningDoThat);
|
||||
|
||||
static void runLocalSandbox(QString contentPath, bool autoShutdown, QString runningMarkerName);
|
||||
};
|
||||
|
||||
#endif // hifi_SandboxUtils_h
|
76
libraries/shared/src/RunningMarker.cpp
Normal file
76
libraries/shared/src/RunningMarker.cpp
Normal file
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// RunningMarker.cpp
|
||||
// libraries/shared/src
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 2016-10-16
|
||||
// Copyright 2016 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 "RunningMarker.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QStandardPaths>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
|
||||
#include "NumericalConstants.h"
|
||||
#include "PathUtils.h"
|
||||
|
||||
|
||||
RunningMarker::RunningMarker(QObject* parent, QString name) :
|
||||
_parent(parent),
|
||||
_name(name)
|
||||
{
|
||||
}
|
||||
|
||||
void RunningMarker::startRunningMarker() {
|
||||
static const int RUNNING_STATE_CHECK_IN_MSECS = MSECS_PER_SECOND;
|
||||
|
||||
// start the nodeThread so its event loop is running
|
||||
QThread* runningMarkerThread = new QThread(_parent);
|
||||
runningMarkerThread->setObjectName("Running Marker Thread");
|
||||
runningMarkerThread->start();
|
||||
|
||||
writeRunningMarkerFiler(); // write the first file, even before timer
|
||||
|
||||
QTimer* runningMarkerTimer = new QTimer(_parent);
|
||||
QObject::connect(runningMarkerTimer, &QTimer::timeout, [=](){
|
||||
writeRunningMarkerFiler();
|
||||
});
|
||||
runningMarkerTimer->start(RUNNING_STATE_CHECK_IN_MSECS);
|
||||
|
||||
// put the time on the thread
|
||||
runningMarkerTimer->moveToThread(runningMarkerThread);
|
||||
}
|
||||
|
||||
RunningMarker::~RunningMarker() {
|
||||
deleteRunningMarkerFile();
|
||||
}
|
||||
|
||||
void RunningMarker::writeRunningMarkerFiler() {
|
||||
QFile runningMarkerFile(getFilePath());
|
||||
|
||||
// always write, even it it exists, so that it touches the files
|
||||
if (runningMarkerFile.open(QIODevice::WriteOnly)) {
|
||||
runningMarkerFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
void RunningMarker::deleteRunningMarkerFile() {
|
||||
QFile runningMarkerFile(getFilePath());
|
||||
if (runningMarkerFile.exists()) {
|
||||
runningMarkerFile.remove();
|
||||
}
|
||||
}
|
||||
|
||||
QString RunningMarker::getFilePath() {
|
||||
return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + _name;
|
||||
}
|
||||
|
||||
QString RunningMarker::getMarkerFilePath(QString name) {
|
||||
return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + name;
|
||||
}
|
||||
|
35
libraries/shared/src/RunningMarker.h
Normal file
35
libraries/shared/src/RunningMarker.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// RunningMarker.h
|
||||
// interface/src
|
||||
//
|
||||
// Created by Brad Hefta-Gaub on 2016-10-15.
|
||||
// Copyright 2016 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_RunningMarker_h
|
||||
#define hifi_RunningMarker_h
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
class RunningMarker {
|
||||
public:
|
||||
RunningMarker(QObject* parent, QString name);
|
||||
~RunningMarker();
|
||||
|
||||
void startRunningMarker();
|
||||
|
||||
QString getFilePath();
|
||||
static QString getMarkerFilePath(QString name);
|
||||
protected:
|
||||
void writeRunningMarkerFiler();
|
||||
void deleteRunningMarkerFile();
|
||||
|
||||
QObject* _parent { nullptr };
|
||||
QString _name;
|
||||
};
|
||||
|
||||
#endif // hifi_RunningMarker_h
|
|
@ -616,6 +616,28 @@ QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir,
|
|||
return fileDialog(map);
|
||||
}
|
||||
|
||||
QString OffscreenUi::existingDirectoryDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QString result;
|
||||
QMetaObject::invokeMethod(this, "existingDirectoryDialog", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QString, result),
|
||||
Q_ARG(QString, caption),
|
||||
Q_ARG(QString, dir),
|
||||
Q_ARG(QString, filter),
|
||||
Q_ARG(QString*, selectedFilter),
|
||||
Q_ARG(QFileDialog::Options, options));
|
||||
return result;
|
||||
}
|
||||
|
||||
QVariantMap map;
|
||||
map.insert("caption", caption);
|
||||
map.insert("dir", QUrl::fromLocalFile(dir));
|
||||
map.insert("filter", filter);
|
||||
map.insert("options", static_cast<int>(options));
|
||||
map.insert("selectDirectory", true);
|
||||
return fileDialog(map);
|
||||
}
|
||||
|
||||
QString OffscreenUi::getOpenFileName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) {
|
||||
return DependencyManager::get<OffscreenUi>()->fileOpenDialog(caption, dir, filter, selectedFilter, options);
|
||||
}
|
||||
|
@ -624,6 +646,10 @@ QString OffscreenUi::getSaveFileName(void* ignored, const QString &caption, cons
|
|||
return DependencyManager::get<OffscreenUi>()->fileSaveDialog(caption, dir, filter, selectedFilter, options);
|
||||
}
|
||||
|
||||
QString OffscreenUi::getExistingDirectory(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) {
|
||||
return DependencyManager::get<OffscreenUi>()->existingDirectoryDialog(caption, dir, filter, selectedFilter, options);
|
||||
}
|
||||
|
||||
bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) {
|
||||
if (!filterEnabled(originalDestination, event)) {
|
||||
return false;
|
||||
|
|
|
@ -115,11 +115,14 @@ public:
|
|||
|
||||
Q_INVOKABLE QString fileOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
Q_INVOKABLE QString existingDirectoryDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
|
||||
// Compatibility with QFileDialog::getOpenFileName
|
||||
static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
// Compatibility with QFileDialog::getSaveFileName
|
||||
static QString getSaveFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
// Compatibility with QFileDialog::getExistingDirectory
|
||||
static QString getExistingDirectory(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
|
||||
Q_INVOKABLE QVariant inputDialog(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant());
|
||||
Q_INVOKABLE QVariant customInputDialog(const Icon icon, const QString& title, const QVariantMap& config);
|
||||
|
|
|
@ -59,13 +59,15 @@ void OculusDisplayPlugin::customizeContext() {
|
|||
|
||||
ovrResult result = ovr_CreateTextureSwapChainGL(_session, &desc, &_textureSwapChain);
|
||||
if (!OVR_SUCCESS(result)) {
|
||||
logFatal("Failed to create swap textures");
|
||||
logCritical("Failed to create swap textures");
|
||||
return;
|
||||
}
|
||||
|
||||
int length = 0;
|
||||
result = ovr_GetTextureSwapChainLength(_session, _textureSwapChain, &length);
|
||||
if (!OVR_SUCCESS(result) || !length) {
|
||||
qFatal("Unable to count swap chain textures");
|
||||
logCritical("Unable to count swap chain textures");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < length; ++i) {
|
||||
GLuint chainTexId;
|
||||
|
@ -83,6 +85,7 @@ void OculusDisplayPlugin::customizeContext() {
|
|||
_sceneLayer.ColorTexture[0] = _textureSwapChain;
|
||||
// not needed since the structure was zeroed on init, but explicit
|
||||
_sceneLayer.ColorTexture[1] = nullptr;
|
||||
_customized = true;
|
||||
}
|
||||
|
||||
void OculusDisplayPlugin::uncustomizeContext() {
|
||||
|
@ -98,10 +101,14 @@ void OculusDisplayPlugin::uncustomizeContext() {
|
|||
ovr_DestroyTextureSwapChain(_session, _textureSwapChain);
|
||||
_textureSwapChain = nullptr;
|
||||
_outputFramebuffer.reset();
|
||||
_customized = false;
|
||||
Parent::uncustomizeContext();
|
||||
}
|
||||
|
||||
void OculusDisplayPlugin::hmdPresent() {
|
||||
if (!_customized) {
|
||||
return;
|
||||
}
|
||||
|
||||
PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex)
|
||||
|
||||
|
|
|
@ -32,5 +32,6 @@ private:
|
|||
static const QString NAME;
|
||||
ovrTextureSwapChain _textureSwapChain;
|
||||
gpu::FramebufferPointer _outputFramebuffer;
|
||||
bool _customized { false };
|
||||
};
|
||||
|
||||
|
|
|
@ -39,12 +39,12 @@ void logWarning(const char* what) {
|
|||
qWarning(oculus) << what << ":" << getError().ErrorString;
|
||||
}
|
||||
|
||||
void logFatal(const char* what) {
|
||||
void logCritical(const char* what) {
|
||||
std::string error("[oculus] ");
|
||||
error += what;
|
||||
error += ": ";
|
||||
error += getError().ErrorString;
|
||||
qFatal(error.c_str());
|
||||
qCritical(error.c_str());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
#include <controllers/Forward.h>
|
||||
|
||||
void logWarning(const char* what);
|
||||
void logFatal(const char* what);
|
||||
void logCritical(const char* what);
|
||||
bool oculusAvailable();
|
||||
ovrSession acquireOculusSession();
|
||||
void releaseOculusSession();
|
||||
|
|
|
@ -2770,10 +2770,10 @@ var handleHandMessages = function(channel, message, sender) {
|
|||
|
||||
Messages.messageReceived.connect(handleHandMessages);
|
||||
|
||||
var BASIC_TIMER_INTERVAL = 20; // 20ms = 50hz good enough
|
||||
var BASIC_TIMER_INTERVAL_MS = 20; // 20ms = 50hz good enough
|
||||
var updateIntervalTimer = Script.setInterval(function(){
|
||||
update();
|
||||
}, BASIC_TIMER_INTERVAL);
|
||||
update(BASIC_TIMER_INTERVAL_MS / 1000);
|
||||
}, BASIC_TIMER_INTERVAL_MS);
|
||||
|
||||
function cleanup() {
|
||||
Script.clearInterval(updateIntervalTimer);
|
||||
|
|
|
@ -128,6 +128,12 @@ function shutdown() {
|
|||
}
|
||||
}
|
||||
|
||||
function forcedShutdown() {
|
||||
if (!isShuttingDown) {
|
||||
shutdownCallback(0);
|
||||
}
|
||||
}
|
||||
|
||||
function shutdownCallback(idx) {
|
||||
if (idx == 0 && !isShuttingDown) {
|
||||
isShuttingDown = true;
|
||||
|
@ -226,6 +232,7 @@ if (shouldQuit) {
|
|||
|
||||
// Check command line arguments to see how to find binaries
|
||||
var argv = require('yargs').argv;
|
||||
|
||||
var pathFinder = require('./modules/path-finder.js');
|
||||
|
||||
var interfacePath = null;
|
||||
|
@ -774,6 +781,7 @@ function maybeShowSplash() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
const trayIconOS = (osType == "Darwin") ? "osx" : "win";
|
||||
var trayIcons = {};
|
||||
trayIcons[ProcessGroupStates.STARTED] = "console-tray-" + trayIconOS + ".png";
|
||||
|
@ -842,6 +850,34 @@ function onContentLoaded() {
|
|||
// start the home server
|
||||
homeServer.start();
|
||||
}
|
||||
|
||||
// If we were launched with the shutdownWatcher option, then we need to watch for the interface app
|
||||
// shutting down. The interface app will regularly update a running state file which we will check.
|
||||
// If the file doesn't exist or stops updating for a significant amount of time, we will shut down.
|
||||
if (argv.shutdownWatcher) {
|
||||
console.log("Shutdown watcher requested... argv.shutdownWatcher:", argv.shutdownWatcher);
|
||||
var MAX_TIME_SINCE_EDIT = 5000; // 5 seconds between updates
|
||||
var firstAttemptToCheck = new Date().getTime();
|
||||
var shutdownWatchInterval = setInterval(function(){
|
||||
var stats = fs.stat(argv.shutdownWatcher, function(err, stats) {
|
||||
if (err) {
|
||||
var sinceFirstCheck = new Date().getTime() - firstAttemptToCheck;
|
||||
if (sinceFirstCheck > MAX_TIME_SINCE_EDIT) {
|
||||
console.log("Running state file is missing, assume interface has shutdown... shutting down snadbox.");
|
||||
forcedShutdown();
|
||||
clearTimeout(shutdownWatchInterval);
|
||||
}
|
||||
} else {
|
||||
var sinceEdit = new Date().getTime() - stats.mtime.getTime();
|
||||
if (sinceEdit > MAX_TIME_SINCE_EDIT) {
|
||||
console.log("Running state of interface hasn't updated in MAX time... shutting down.");
|
||||
forcedShutdown();
|
||||
clearTimeout(shutdownWatchInterval);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue