45 changed files with 1577 additions and 39 deletions
-
4.travis/common/travis-ci.env
-
2.travis/linux/build.sh
-
2.travis/linux/docker.sh
-
2.travis/macos/build.sh
-
11CMakeLists.txt
-
5appveyor.yml
-
1externals/discord-rpc
-
1externals/libressl
-
3src/CMakeLists.txt
-
3src/common/CMakeLists.txt
-
41src/common/detached_tasks.cpp
-
39src/common/detached_tasks.h
-
24src/common/web_result.h
-
3src/core/CMakeLists.txt
-
6src/core/settings.h
-
47src/core/telemetry_session.cpp
-
5src/core/telemetry_session.h
-
16src/web_service/CMakeLists.txt
-
18src/web_service/json.h
-
94src/web_service/telemetry_json.cpp
-
59src/web_service/telemetry_json.h
-
27src/web_service/verify_login.cpp
-
22src/web_service/verify_login.h
-
147src/web_service/web_backend.cpp
-
91src/web_service/web_backend.h
-
16src/yuzu/CMakeLists.txt
-
61src/yuzu/compatdb.cpp
-
27src/yuzu/compatdb.h
-
215src/yuzu/compatdb.ui
-
18src/yuzu/configuration/config.cpp
-
11src/yuzu/configuration/configure.ui
-
1src/yuzu/configuration/configure_dialog.cpp
-
121src/yuzu/configuration/configure_web.cpp
-
38src/yuzu/configuration/configure_web.h
-
206src/yuzu/configuration/configure_web.ui
-
25src/yuzu/discord.h
-
52src/yuzu/discord_impl.cpp
-
20src/yuzu/discord_impl.h
-
82src/yuzu/main.cpp
-
10src/yuzu/main.h
-
16src/yuzu/main.ui
-
3src/yuzu/ui_settings.h
-
8src/yuzu_cmd/config.cpp
-
6src/yuzu_cmd/default_ini.h
-
3src/yuzu_cmd/yuzu.cpp
@ -1,4 +1,4 @@ |
|||||
#!/bin/bash -ex |
#!/bin/bash -ex |
||||
|
|
||||
mkdir -p "$HOME/.ccache" |
mkdir -p "$HOME/.ccache" |
||||
docker run --env-file .travis/common/travis-ci.env -v $(pwd):/yuzu -v "$HOME/.ccache":/root/.ccache ubuntu:18.04 /bin/bash /yuzu/.travis/linux/docker.sh |
|
||||
|
docker run -e ENABLE_COMPATIBILITY_REPORTING --env-file .travis/common/travis-ci.env -v $(pwd):/yuzu -v "$HOME/.ccache":/root/.ccache ubuntu:18.04 /bin/bash /yuzu/.travis/linux/docker.sh |
||||
@ -0,0 +1,41 @@ |
|||||
|
// Copyright 2018 Citra Emulator Project
|
||||
|
// Licensed under GPLv2 or any later version
|
||||
|
// Refer to the license.txt file included.
|
||||
|
|
||||
|
#include <thread>
|
||||
|
#include "common/assert.h"
|
||||
|
#include "common/detached_tasks.h"
|
||||
|
|
||||
|
namespace Common { |
||||
|
|
||||
|
DetachedTasks* DetachedTasks::instance = nullptr; |
||||
|
|
||||
|
DetachedTasks::DetachedTasks() { |
||||
|
ASSERT(instance == nullptr); |
||||
|
instance = this; |
||||
|
} |
||||
|
|
||||
|
void DetachedTasks::WaitForAllTasks() { |
||||
|
std::unique_lock<std::mutex> lock(mutex); |
||||
|
cv.wait(lock, [this]() { return count == 0; }); |
||||
|
} |
||||
|
|
||||
|
DetachedTasks::~DetachedTasks() { |
||||
|
std::unique_lock<std::mutex> lock(mutex); |
||||
|
ASSERT(count == 0); |
||||
|
instance = nullptr; |
||||
|
} |
||||
|
|
||||
|
void DetachedTasks::AddTask(std::function<void()> task) { |
||||
|
std::unique_lock<std::mutex> lock(instance->mutex); |
||||
|
++instance->count; |
||||
|
std::thread([task{std::move(task)}]() { |
||||
|
task(); |
||||
|
std::unique_lock<std::mutex> lock(instance->mutex); |
||||
|
--instance->count; |
||||
|
std::notify_all_at_thread_exit(instance->cv, std::move(lock)); |
||||
|
}) |
||||
|
.detach(); |
||||
|
} |
||||
|
|
||||
|
} // namespace Common
|
||||
@ -0,0 +1,39 @@ |
|||||
|
// Copyright 2018 Citra Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
#include <condition_variable> |
||||
|
#include <functional> |
||||
|
|
||||
|
namespace Common { |
||||
|
|
||||
|
/** |
||||
|
* A background manager which ensures that all detached task is finished before program exits. |
||||
|
* |
||||
|
* Some tasks, telemetry submission for example, prefer executing asynchronously and don't care |
||||
|
* about the result. These tasks are suitable for std::thread::detach(). However, this is unsafe if |
||||
|
* the task is launched just before the program exits (which is a common case for telemetry), so we |
||||
|
* need to block on these tasks on program exit. |
||||
|
* |
||||
|
* To make detached task safe, a single DetachedTasks object should be placed in the main(), and |
||||
|
* call WaitForAllTasks() after all program execution but before global/static variable destruction. |
||||
|
* Any potentially unsafe detached task should be executed via DetachedTasks::AddTask. |
||||
|
*/ |
||||
|
class DetachedTasks { |
||||
|
public: |
||||
|
DetachedTasks(); |
||||
|
~DetachedTasks(); |
||||
|
void WaitForAllTasks(); |
||||
|
|
||||
|
static void AddTask(std::function<void()> task); |
||||
|
|
||||
|
private: |
||||
|
static DetachedTasks* instance; |
||||
|
|
||||
|
std::condition_variable cv; |
||||
|
std::mutex mutex; |
||||
|
int count = 0; |
||||
|
}; |
||||
|
|
||||
|
} // namespace Common |
||||
@ -0,0 +1,24 @@ |
|||||
|
// Copyright 2018 yuzu Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <string> |
||||
|
|
||||
|
namespace Common { |
||||
|
struct WebResult { |
||||
|
enum class Code : u32 { |
||||
|
Success, |
||||
|
InvalidURL, |
||||
|
CredentialsMissing, |
||||
|
LibError, |
||||
|
HttpError, |
||||
|
WrongContent, |
||||
|
NoWebservice, |
||||
|
}; |
||||
|
Code result_code; |
||||
|
std::string result_string; |
||||
|
std::string returned_data; |
||||
|
}; |
||||
|
} // namespace Commo |
||||
@ -0,0 +1,16 @@ |
|||||
|
add_library(web_service STATIC |
||||
|
telemetry_json.cpp |
||||
|
telemetry_json.h |
||||
|
verify_login.cpp |
||||
|
verify_login.h |
||||
|
web_backend.cpp |
||||
|
web_backend.h |
||||
|
) |
||||
|
|
||||
|
create_target_directory_groups(web_service) |
||||
|
|
||||
|
get_directory_property(OPENSSL_LIBS |
||||
|
DIRECTORY ${CMAKE_SOURCE_DIR}/externals/libressl |
||||
|
DEFINITION OPENSSL_LIBS) |
||||
|
add_definitions(-DCPPHTTPLIB_OPENSSL_SUPPORT) |
||||
|
target_link_libraries(web_service PUBLIC common json-headers ${OPENSSL_LIBS} httplib lurlparser) |
||||
@ -0,0 +1,18 @@ |
|||||
|
// Copyright 2018 Citra Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
// This hack is needed to support json.hpp on platforms where the C++17 stdlib |
||||
|
// lacks std::string_view. See https://github.com/nlohmann/json/issues/735. |
||||
|
// clang-format off |
||||
|
#if !__has_include(<string_view>) && __has_include(<experimental/string_view>) |
||||
|
# include <experimental/string_view> |
||||
|
# define string_view experimental::string_view |
||||
|
# include <json.hpp> |
||||
|
# undef string_view |
||||
|
#else |
||||
|
# include <json.hpp> |
||||
|
#endif |
||||
|
// clang-format on |
||||
@ -0,0 +1,94 @@ |
|||||
|
// Copyright 2017 Citra Emulator Project
|
||||
|
// Licensed under GPLv2 or any later version
|
||||
|
// Refer to the license.txt file included.
|
||||
|
|
||||
|
#include <thread>
|
||||
|
#include "common/assert.h"
|
||||
|
#include "common/detached_tasks.h"
|
||||
|
#include "web_service/telemetry_json.h"
|
||||
|
#include "web_service/web_backend.h"
|
||||
|
|
||||
|
namespace WebService { |
||||
|
|
||||
|
template <class T> |
||||
|
void TelemetryJson::Serialize(Telemetry::FieldType type, const std::string& name, T value) { |
||||
|
sections[static_cast<u8>(type)][name] = value; |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::SerializeSection(Telemetry::FieldType type, const std::string& name) { |
||||
|
TopSection()[name] = sections[static_cast<unsigned>(type)]; |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<bool>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<double>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<float>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<u8>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<u16>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<u32>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<u64>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<s8>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<s16>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<s32>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<s64>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<std::string>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<const char*>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), std::string(field.GetValue())); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Visit(const Telemetry::Field<std::chrono::microseconds>& field) { |
||||
|
Serialize(field.GetType(), field.GetName(), field.GetValue().count()); |
||||
|
} |
||||
|
|
||||
|
void TelemetryJson::Complete() { |
||||
|
SerializeSection(Telemetry::FieldType::App, "App"); |
||||
|
SerializeSection(Telemetry::FieldType::Session, "Session"); |
||||
|
SerializeSection(Telemetry::FieldType::Performance, "Performance"); |
||||
|
SerializeSection(Telemetry::FieldType::UserFeedback, "UserFeedback"); |
||||
|
SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig"); |
||||
|
SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem"); |
||||
|
|
||||
|
auto content = TopSection().dump(); |
||||
|
// Send the telemetry async but don't handle the errors since they were written to the log
|
||||
|
Common::DetachedTasks::AddTask( |
||||
|
[host{this->host}, username{this->username}, token{this->token}, content]() { |
||||
|
Client{host, username, token}.PostJson("/telemetry", content, true); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
} // namespace WebService
|
||||
@ -0,0 +1,59 @@ |
|||||
|
// Copyright 2017 Citra Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <array> |
||||
|
#include <string> |
||||
|
#include "common/telemetry.h" |
||||
|
#include "common/web_result.h" |
||||
|
#include "web_service/json.h" |
||||
|
|
||||
|
namespace WebService { |
||||
|
|
||||
|
/** |
||||
|
* Implementation of VisitorInterface that serialized telemetry into JSON, and submits it to the |
||||
|
* yuzu web service |
||||
|
*/ |
||||
|
class TelemetryJson : public Telemetry::VisitorInterface { |
||||
|
public: |
||||
|
TelemetryJson(const std::string& host, const std::string& username, const std::string& token) |
||||
|
: host(host), username(username), token(token) {} |
||||
|
~TelemetryJson() = default; |
||||
|
|
||||
|
void Visit(const Telemetry::Field<bool>& field) override; |
||||
|
void Visit(const Telemetry::Field<double>& field) override; |
||||
|
void Visit(const Telemetry::Field<float>& field) override; |
||||
|
void Visit(const Telemetry::Field<u8>& field) override; |
||||
|
void Visit(const Telemetry::Field<u16>& field) override; |
||||
|
void Visit(const Telemetry::Field<u32>& field) override; |
||||
|
void Visit(const Telemetry::Field<u64>& field) override; |
||||
|
void Visit(const Telemetry::Field<s8>& field) override; |
||||
|
void Visit(const Telemetry::Field<s16>& field) override; |
||||
|
void Visit(const Telemetry::Field<s32>& field) override; |
||||
|
void Visit(const Telemetry::Field<s64>& field) override; |
||||
|
void Visit(const Telemetry::Field<std::string>& field) override; |
||||
|
void Visit(const Telemetry::Field<const char*>& field) override; |
||||
|
void Visit(const Telemetry::Field<std::chrono::microseconds>& field) override; |
||||
|
|
||||
|
void Complete() override; |
||||
|
|
||||
|
private: |
||||
|
nlohmann::json& TopSection() { |
||||
|
return sections[static_cast<u8>(Telemetry::FieldType::None)]; |
||||
|
} |
||||
|
|
||||
|
template <class T> |
||||
|
void Serialize(Telemetry::FieldType type, const std::string& name, T value); |
||||
|
|
||||
|
void SerializeSection(Telemetry::FieldType type, const std::string& name); |
||||
|
|
||||
|
nlohmann::json output; |
||||
|
std::array<nlohmann::json, 7> sections; |
||||
|
std::string host; |
||||
|
std::string username; |
||||
|
std::string token; |
||||
|
}; |
||||
|
|
||||
|
} // namespace WebService |
||||
@ -0,0 +1,27 @@ |
|||||
|
// Copyright 2017 Citra Emulator Project
|
||||
|
// Licensed under GPLv2 or any later version
|
||||
|
// Refer to the license.txt file included.
|
||||
|
|
||||
|
#include "web_service/json.h"
|
||||
|
#include "web_service/verify_login.h"
|
||||
|
#include "web_service/web_backend.h"
|
||||
|
|
||||
|
namespace WebService { |
||||
|
|
||||
|
bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token) { |
||||
|
Client client(host, username, token); |
||||
|
auto reply = client.GetJson("/profile", false).returned_data; |
||||
|
if (reply.empty()) { |
||||
|
return false; |
||||
|
} |
||||
|
nlohmann::json json = nlohmann::json::parse(reply); |
||||
|
const auto iter = json.find("username"); |
||||
|
|
||||
|
if (iter == json.end()) { |
||||
|
return username.empty(); |
||||
|
} |
||||
|
|
||||
|
return username == *iter; |
||||
|
} |
||||
|
|
||||
|
} // namespace WebService
|
||||
@ -0,0 +1,22 @@ |
|||||
|
// Copyright 2017 Citra Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <functional> |
||||
|
#include <future> |
||||
|
#include <string> |
||||
|
|
||||
|
namespace WebService { |
||||
|
|
||||
|
/** |
||||
|
* Checks if username and token is valid |
||||
|
* @param host the web API URL |
||||
|
* @param username yuzu username to use for authentication. |
||||
|
* @param token yuzu token to use for authentication. |
||||
|
* @returns a bool indicating whether the verification succeeded |
||||
|
*/ |
||||
|
bool VerifyLogin(const std::string& host, const std::string& username, const std::string& token); |
||||
|
|
||||
|
} // namespace WebService |
||||
@ -0,0 +1,147 @@ |
|||||
|
// Copyright 2017 Citra Emulator Project
|
||||
|
// Licensed under GPLv2 or any later version
|
||||
|
// Refer to the license.txt file included.
|
||||
|
|
||||
|
#include <cstdlib>
|
||||
|
#include <string>
|
||||
|
#include <thread>
|
||||
|
#include <LUrlParser.h>
|
||||
|
#include "common/logging/log.h"
|
||||
|
#include "common/web_result.h"
|
||||
|
#include "core/settings.h"
|
||||
|
#include "web_service/web_backend.h"
|
||||
|
|
||||
|
namespace WebService { |
||||
|
|
||||
|
static constexpr char API_VERSION[]{"1"}; |
||||
|
|
||||
|
constexpr int HTTP_PORT = 80; |
||||
|
constexpr int HTTPS_PORT = 443; |
||||
|
|
||||
|
constexpr int TIMEOUT_SECONDS = 30; |
||||
|
|
||||
|
Client::JWTCache Client::jwt_cache{}; |
||||
|
|
||||
|
Client::Client(const std::string& host, const std::string& username, const std::string& token) |
||||
|
: host(host), username(username), token(token) { |
||||
|
if (username == jwt_cache.username && token == jwt_cache.token) { |
||||
|
jwt = jwt_cache.jwt; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Common::WebResult Client::GenericJson(const std::string& method, const std::string& path, |
||||
|
const std::string& data, const std::string& jwt, |
||||
|
const std::string& username, const std::string& token) { |
||||
|
if (cli == nullptr) { |
||||
|
auto parsedUrl = LUrlParser::clParseURL::ParseURL(host); |
||||
|
int port; |
||||
|
if (parsedUrl.m_Scheme == "http") { |
||||
|
if (!parsedUrl.GetPort(&port)) { |
||||
|
port = HTTP_PORT; |
||||
|
} |
||||
|
cli = |
||||
|
std::make_unique<httplib::Client>(parsedUrl.m_Host.c_str(), port, TIMEOUT_SECONDS); |
||||
|
} else if (parsedUrl.m_Scheme == "https") { |
||||
|
if (!parsedUrl.GetPort(&port)) { |
||||
|
port = HTTPS_PORT; |
||||
|
} |
||||
|
cli = std::make_unique<httplib::SSLClient>(parsedUrl.m_Host.c_str(), port, |
||||
|
TIMEOUT_SECONDS); |
||||
|
} else { |
||||
|
LOG_ERROR(WebService, "Bad URL scheme {}", parsedUrl.m_Scheme); |
||||
|
return Common::WebResult{Common::WebResult::Code::InvalidURL, "Bad URL scheme"}; |
||||
|
} |
||||
|
} |
||||
|
if (cli == nullptr) { |
||||
|
LOG_ERROR(WebService, "Invalid URL {}", host + path); |
||||
|
return Common::WebResult{Common::WebResult::Code::InvalidURL, "Invalid URL"}; |
||||
|
} |
||||
|
|
||||
|
httplib::Headers params; |
||||
|
if (!jwt.empty()) { |
||||
|
params = { |
||||
|
{std::string("Authorization"), fmt::format("Bearer {}", jwt)}, |
||||
|
}; |
||||
|
} else if (!username.empty()) { |
||||
|
params = { |
||||
|
{std::string("x-username"), username}, |
||||
|
{std::string("x-token"), token}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
params.emplace(std::string("api-version"), std::string(API_VERSION)); |
||||
|
if (method != "GET") { |
||||
|
params.emplace(std::string("Content-Type"), std::string("application/json")); |
||||
|
}; |
||||
|
|
||||
|
httplib::Request request; |
||||
|
request.method = method; |
||||
|
request.path = path; |
||||
|
request.headers = params; |
||||
|
request.body = data; |
||||
|
|
||||
|
httplib::Response response; |
||||
|
|
||||
|
if (!cli->send(request, response)) { |
||||
|
LOG_ERROR(WebService, "{} to {} returned null", method, host + path); |
||||
|
return Common::WebResult{Common::WebResult::Code::LibError, "Null response"}; |
||||
|
} |
||||
|
|
||||
|
if (response.status >= 400) { |
||||
|
LOG_ERROR(WebService, "{} to {} returned error status code: {}", method, host + path, |
||||
|
response.status); |
||||
|
return Common::WebResult{Common::WebResult::Code::HttpError, |
||||
|
std::to_string(response.status)}; |
||||
|
} |
||||
|
|
||||
|
auto content_type = response.headers.find("content-type"); |
||||
|
|
||||
|
if (content_type == response.headers.end()) { |
||||
|
LOG_ERROR(WebService, "{} to {} returned no content", method, host + path); |
||||
|
return Common::WebResult{Common::WebResult::Code::WrongContent, ""}; |
||||
|
} |
||||
|
|
||||
|
if (content_type->second.find("application/json") == std::string::npos && |
||||
|
content_type->second.find("text/html; charset=utf-8") == std::string::npos) { |
||||
|
LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path, |
||||
|
content_type->second); |
||||
|
return Common::WebResult{Common::WebResult::Code::WrongContent, "Wrong content"}; |
||||
|
} |
||||
|
return Common::WebResult{Common::WebResult::Code::Success, "", response.body}; |
||||
|
} |
||||
|
|
||||
|
void Client::UpdateJWT() { |
||||
|
if (!username.empty() && !token.empty()) { |
||||
|
auto result = GenericJson("POST", "/jwt/internal", "", "", username, token); |
||||
|
if (result.result_code != Common::WebResult::Code::Success) { |
||||
|
LOG_ERROR(WebService, "UpdateJWT failed"); |
||||
|
} else { |
||||
|
jwt_cache.username = username; |
||||
|
jwt_cache.token = token; |
||||
|
jwt_cache.jwt = jwt = result.returned_data; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Common::WebResult Client::GenericJson(const std::string& method, const std::string& path, |
||||
|
const std::string& data, bool allow_anonymous) { |
||||
|
if (jwt.empty()) { |
||||
|
UpdateJWT(); |
||||
|
} |
||||
|
|
||||
|
if (jwt.empty() && !allow_anonymous) { |
||||
|
LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); |
||||
|
return Common::WebResult{Common::WebResult::Code::CredentialsMissing, "Credentials needed"}; |
||||
|
} |
||||
|
|
||||
|
auto result = GenericJson(method, path, data, jwt); |
||||
|
if (result.result_string == "401") { |
||||
|
// Try again with new JWT
|
||||
|
UpdateJWT(); |
||||
|
result = GenericJson(method, path, data, jwt); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
} // namespace WebService
|
||||
@ -0,0 +1,91 @@ |
|||||
|
// Copyright 2017 Citra Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <functional> |
||||
|
#include <future> |
||||
|
#include <string> |
||||
|
#include <tuple> |
||||
|
#include <httplib.h> |
||||
|
#include "common/common_types.h" |
||||
|
#include "common/web_result.h" |
||||
|
|
||||
|
namespace httplib { |
||||
|
class Client; |
||||
|
} |
||||
|
|
||||
|
namespace WebService { |
||||
|
|
||||
|
class Client { |
||||
|
public: |
||||
|
Client(const std::string& host, const std::string& username, const std::string& token); |
||||
|
|
||||
|
/** |
||||
|
* Posts JSON to the specified path. |
||||
|
* @param path the URL segment after the host address. |
||||
|
* @param data String of JSON data to use for the body of the POST request. |
||||
|
* @param allow_anonymous If true, allow anonymous unauthenticated requests. |
||||
|
* @return the result of the request. |
||||
|
*/ |
||||
|
Common::WebResult PostJson(const std::string& path, const std::string& data, |
||||
|
bool allow_anonymous) { |
||||
|
return GenericJson("POST", path, data, allow_anonymous); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Gets JSON from the specified path. |
||||
|
* @param path the URL segment after the host address. |
||||
|
* @param allow_anonymous If true, allow anonymous unauthenticated requests. |
||||
|
* @return the result of the request. |
||||
|
*/ |
||||
|
Common::WebResult GetJson(const std::string& path, bool allow_anonymous) { |
||||
|
return GenericJson("GET", path, "", allow_anonymous); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Deletes JSON to the specified path. |
||||
|
* @param path the URL segment after the host address. |
||||
|
* @param data String of JSON data to use for the body of the DELETE request. |
||||
|
* @param allow_anonymous If true, allow anonymous unauthenticated requests. |
||||
|
* @return the result of the request. |
||||
|
*/ |
||||
|
Common::WebResult DeleteJson(const std::string& path, const std::string& data, |
||||
|
bool allow_anonymous) { |
||||
|
return GenericJson("DELETE", path, data, allow_anonymous); |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
/// A generic function handles POST, GET and DELETE request together |
||||
|
Common::WebResult GenericJson(const std::string& method, const std::string& path, |
||||
|
const std::string& data, bool allow_anonymous); |
||||
|
|
||||
|
/** |
||||
|
* A generic function with explicit authentication method specified |
||||
|
* JWT is used if the jwt parameter is not empty |
||||
|
* username + token is used if jwt is empty but username and token are not empty |
||||
|
* anonymous if all of jwt, username and token are empty |
||||
|
*/ |
||||
|
Common::WebResult GenericJson(const std::string& method, const std::string& path, |
||||
|
const std::string& data, const std::string& jwt = "", |
||||
|
const std::string& username = "", const std::string& token = ""); |
||||
|
|
||||
|
// Retrieve a new JWT from given username and token |
||||
|
void UpdateJWT(); |
||||
|
|
||||
|
std::string host; |
||||
|
std::string username; |
||||
|
std::string token; |
||||
|
std::string jwt; |
||||
|
std::unique_ptr<httplib::Client> cli; |
||||
|
|
||||
|
struct JWTCache { |
||||
|
std::string username; |
||||
|
std::string token; |
||||
|
std::string jwt; |
||||
|
}; |
||||
|
static JWTCache jwt_cache; |
||||
|
}; |
||||
|
|
||||
|
} // namespace WebService |
||||
@ -0,0 +1,61 @@ |
|||||
|
// Copyright 2017 Citra Emulator Project
|
||||
|
// Licensed under GPLv2 or any later version
|
||||
|
// Refer to the license.txt file included.
|
||||
|
|
||||
|
#include <QButtonGroup>
|
||||
|
#include <QMessageBox>
|
||||
|
#include <QPushButton>
|
||||
|
#include "common/logging/log.h"
|
||||
|
#include "common/telemetry.h"
|
||||
|
#include "core/core.h"
|
||||
|
#include "core/telemetry_session.h"
|
||||
|
#include "ui_compatdb.h"
|
||||
|
#include "yuzu/compatdb.h"
|
||||
|
|
||||
|
CompatDB::CompatDB(QWidget* parent) |
||||
|
: QWizard(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint), |
||||
|
ui{std::make_unique<Ui::CompatDB>()} { |
||||
|
ui->setupUi(this); |
||||
|
connect(ui->radioButton_Perfect, &QRadioButton::clicked, this, &CompatDB::EnableNext); |
||||
|
connect(ui->radioButton_Great, &QRadioButton::clicked, this, &CompatDB::EnableNext); |
||||
|
connect(ui->radioButton_Okay, &QRadioButton::clicked, this, &CompatDB::EnableNext); |
||||
|
connect(ui->radioButton_Bad, &QRadioButton::clicked, this, &CompatDB::EnableNext); |
||||
|
connect(ui->radioButton_IntroMenu, &QRadioButton::clicked, this, &CompatDB::EnableNext); |
||||
|
connect(ui->radioButton_WontBoot, &QRadioButton::clicked, this, &CompatDB::EnableNext); |
||||
|
connect(button(NextButton), &QPushButton::clicked, this, &CompatDB::Submit); |
||||
|
} |
||||
|
|
||||
|
CompatDB::~CompatDB() = default; |
||||
|
|
||||
|
enum class CompatDBPage { Intro = 0, Selection = 1, Final = 2 }; |
||||
|
|
||||
|
void CompatDB::Submit() { |
||||
|
QButtonGroup* compatibility = new QButtonGroup(this); |
||||
|
compatibility->addButton(ui->radioButton_Perfect, 0); |
||||
|
compatibility->addButton(ui->radioButton_Great, 1); |
||||
|
compatibility->addButton(ui->radioButton_Okay, 2); |
||||
|
compatibility->addButton(ui->radioButton_Bad, 3); |
||||
|
compatibility->addButton(ui->radioButton_IntroMenu, 4); |
||||
|
compatibility->addButton(ui->radioButton_WontBoot, 5); |
||||
|
switch ((static_cast<CompatDBPage>(currentId()))) { |
||||
|
case CompatDBPage::Selection: |
||||
|
if (compatibility->checkedId() == -1) { |
||||
|
button(NextButton)->setEnabled(false); |
||||
|
} |
||||
|
break; |
||||
|
case CompatDBPage::Final: |
||||
|
LOG_DEBUG(Frontend, "Compatibility Rating: {}", compatibility->checkedId()); |
||||
|
Core::Telemetry().AddField(Telemetry::FieldType::UserFeedback, "Compatibility", |
||||
|
compatibility->checkedId()); |
||||
|
// older versions of QT don't support the "NoCancelButtonOnLastPage" option, this is a
|
||||
|
// workaround
|
||||
|
button(QWizard::CancelButton)->setVisible(false); |
||||
|
break; |
||||
|
default: |
||||
|
LOG_ERROR(Frontend, "Unexpected page: {}", currentId()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void CompatDB::EnableNext() { |
||||
|
button(NextButton)->setEnabled(true); |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
// Copyright 2017 Citra Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <memory> |
||||
|
#include <QWizard> |
||||
|
|
||||
|
namespace Ui { |
||||
|
class CompatDB; |
||||
|
} |
||||
|
|
||||
|
class CompatDB : public QWizard { |
||||
|
Q_OBJECT |
||||
|
|
||||
|
public: |
||||
|
explicit CompatDB(QWidget* parent = nullptr); |
||||
|
~CompatDB(); |
||||
|
|
||||
|
private: |
||||
|
std::unique_ptr<Ui::CompatDB> ui; |
||||
|
|
||||
|
private slots: |
||||
|
void Submit(); |
||||
|
void EnableNext(); |
||||
|
}; |
||||
@ -0,0 +1,215 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<ui version="4.0"> |
||||
|
<class>CompatDB</class> |
||||
|
<widget class="QWizard" name="CompatDB"> |
||||
|
<property name="geometry"> |
||||
|
<rect> |
||||
|
<x>0</x> |
||||
|
<y>0</y> |
||||
|
<width>600</width> |
||||
|
<height>482</height> |
||||
|
</rect> |
||||
|
</property> |
||||
|
<property name="minimumSize"> |
||||
|
<size> |
||||
|
<width>500</width> |
||||
|
<height>410</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
<property name="windowTitle"> |
||||
|
<string>Report Compatibility</string> |
||||
|
</property> |
||||
|
<property name="options"> |
||||
|
<set>QWizard::DisabledBackButtonOnLastPage|QWizard::HelpButtonOnRight|QWizard::NoBackButtonOnStartPage</set> |
||||
|
</property> |
||||
|
<widget class="QWizardPage" name="wizard_Info"> |
||||
|
<property name="title"> |
||||
|
<string>Report Game Compatibility</string> |
||||
|
</property> |
||||
|
<attribute name="pageId"> |
||||
|
<string notr="true">0</string> |
||||
|
</attribute> |
||||
|
<layout class="QVBoxLayout" name="verticalLayout"> |
||||
|
<item> |
||||
|
<widget class="QLabel" name="lbl_Spiel"> |
||||
|
<property name="text"> |
||||
|
<string><html><head/><body><p><span style=" font-size:10pt;">Should you choose to submit a test case to the </span><a href="https://yuzu-emu.org/game/"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">yuzu Compatibility List</span></a><span style=" font-size:10pt;">, The following information will be collected and displayed on the site:</span></p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Hardware Information (CPU / GPU / Operating System)</li><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Which version of yuzu you are running</li><li style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The connected yuzu account</li></ul></body></html></string> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
<property name="openExternalLinks"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<spacer name="verticalSpacer_2"> |
||||
|
<property name="orientation"> |
||||
|
<enum>Qt::Vertical</enum> |
||||
|
</property> |
||||
|
<property name="sizeHint" stdset="0"> |
||||
|
<size> |
||||
|
<width>20</width> |
||||
|
<height>0</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</spacer> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</widget> |
||||
|
<widget class="QWizardPage" name="wizard_Report"> |
||||
|
<property name="title"> |
||||
|
<string>Report Game Compatibility</string> |
||||
|
</property> |
||||
|
<attribute name="pageId"> |
||||
|
<string notr="true">1</string> |
||||
|
</attribute> |
||||
|
<layout class="QFormLayout" name="formLayout"> |
||||
|
<item row="2" column="0"> |
||||
|
<widget class="QRadioButton" name="radioButton_Perfect"> |
||||
|
<property name="text"> |
||||
|
<string>Perfect</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="2" column="1"> |
||||
|
<widget class="QLabel" name="lbl_Perfect"> |
||||
|
<property name="text"> |
||||
|
<string><html><head/><body><p>Game functions flawlessly with no audio or graphical glitches.</p></body></html></string> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="4" column="0"> |
||||
|
<widget class="QRadioButton" name="radioButton_Great"> |
||||
|
<property name="text"> |
||||
|
<string>Great </string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="4" column="1"> |
||||
|
<widget class="QLabel" name="lbl_Great"> |
||||
|
<property name="text"> |
||||
|
<string><html><head/><body><p>Game functions with minor graphical or audio glitches and is playable from start to finish. May require some workarounds.</p></body></html></string> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="5" column="0"> |
||||
|
<widget class="QRadioButton" name="radioButton_Okay"> |
||||
|
<property name="text"> |
||||
|
<string>Okay</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="5" column="1"> |
||||
|
<widget class="QLabel" name="lbl_Okay"> |
||||
|
<property name="text"> |
||||
|
<string><html><head/><body><p>Game functions with major graphical or audio glitches, but game is playable from start to finish with workarounds.</p></body></html></string> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="6" column="0"> |
||||
|
<widget class="QRadioButton" name="radioButton_Bad"> |
||||
|
<property name="text"> |
||||
|
<string>Bad</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="6" column="1"> |
||||
|
<widget class="QLabel" name="lbl_Bad"> |
||||
|
<property name="text"> |
||||
|
<string><html><head/><body><p>Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches even with workarounds.</p></body></html></string> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="7" column="0"> |
||||
|
<widget class="QRadioButton" name="radioButton_IntroMenu"> |
||||
|
<property name="text"> |
||||
|
<string>Intro/Menu</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="7" column="1"> |
||||
|
<widget class="QLabel" name="lbl_IntroMenu"> |
||||
|
<property name="text"> |
||||
|
<string><html><head/><body><p>Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start Screen.</p></body></html></string> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="8" column="0"> |
||||
|
<widget class="QRadioButton" name="radioButton_WontBoot"> |
||||
|
<property name="text"> |
||||
|
<string>Won't Boot</string> |
||||
|
</property> |
||||
|
<property name="checkable"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
<property name="checked"> |
||||
|
<bool>false</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="8" column="1"> |
||||
|
<widget class="QLabel" name="lbl_WontBoot"> |
||||
|
<property name="text"> |
||||
|
<string><html><head/><body><p>The game crashes when attempting to startup.</p></body></html></string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="0" column="0" colspan="2"> |
||||
|
<widget class="QLabel" name="lbl_Independent"> |
||||
|
<property name="font"> |
||||
|
<font> |
||||
|
<pointsize>10</pointsize> |
||||
|
</font> |
||||
|
</property> |
||||
|
<property name="text"> |
||||
|
<string><html><head/><body><p>Independent of speed or performance, how well does this game play from start to finish on this version of yuzu?</p></body></html></string> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="1" column="0" colspan="2"> |
||||
|
<spacer name="verticalSpacer"> |
||||
|
<property name="orientation"> |
||||
|
<enum>Qt::Vertical</enum> |
||||
|
</property> |
||||
|
<property name="sizeHint" stdset="0"> |
||||
|
<size> |
||||
|
<width>20</width> |
||||
|
<height>0</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</spacer> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</widget> |
||||
|
<widget class="QWizardPage" name="wizard_ThankYou"> |
||||
|
<property name="title"> |
||||
|
<string>Thank you for your submission!</string> |
||||
|
</property> |
||||
|
<attribute name="pageId"> |
||||
|
<string notr="true">2</string> |
||||
|
</attribute> |
||||
|
</widget> |
||||
|
</widget> |
||||
|
<resources/> |
||||
|
<connections/> |
||||
|
</ui> |
||||
@ -0,0 +1,121 @@ |
|||||
|
// Copyright 2017 Citra Emulator Project
|
||||
|
// Licensed under GPLv2 or any later version
|
||||
|
// Refer to the license.txt file included.
|
||||
|
|
||||
|
#include <QIcon>
|
||||
|
#include <QMessageBox>
|
||||
|
#include <QtConcurrent/QtConcurrentRun>
|
||||
|
#include "core/settings.h"
|
||||
|
#include "core/telemetry_session.h"
|
||||
|
#include "ui_configure_web.h"
|
||||
|
#include "yuzu/configuration/configure_web.h"
|
||||
|
#include "yuzu/ui_settings.h"
|
||||
|
|
||||
|
ConfigureWeb::ConfigureWeb(QWidget* parent) |
||||
|
: QWidget(parent), ui(std::make_unique<Ui::ConfigureWeb>()) { |
||||
|
ui->setupUi(this); |
||||
|
connect(ui->button_regenerate_telemetry_id, &QPushButton::clicked, this, |
||||
|
&ConfigureWeb::RefreshTelemetryID); |
||||
|
connect(ui->button_verify_login, &QPushButton::clicked, this, &ConfigureWeb::VerifyLogin); |
||||
|
connect(&verify_watcher, &QFutureWatcher<bool>::finished, this, &ConfigureWeb::OnLoginVerified); |
||||
|
|
||||
|
#ifndef USE_DISCORD_PRESENCE
|
||||
|
ui->discord_group->setVisible(false); |
||||
|
#endif
|
||||
|
this->setConfiguration(); |
||||
|
} |
||||
|
|
||||
|
ConfigureWeb::~ConfigureWeb() {} |
||||
|
|
||||
|
void ConfigureWeb::setConfiguration() { |
||||
|
ui->web_credentials_disclaimer->setWordWrap(true); |
||||
|
ui->telemetry_learn_more->setOpenExternalLinks(true); |
||||
|
ui->telemetry_learn_more->setText(tr("<a " |
||||
|
"href='https://citra-emu.org/entry/" |
||||
|
"telemetry-and-why-thats-a-good-thing/'><span " |
||||
|
"style=\"text-decoration: underline; " |
||||
|
"color:#039be5;\">Learn more</span></a>")); |
||||
|
|
||||
|
ui->web_signup_link->setOpenExternalLinks(true); |
||||
|
ui->web_signup_link->setText( |
||||
|
tr("<a href='https://services.citra-emu.org/'><span style=\"text-decoration: underline; " |
||||
|
"color:#039be5;\">Sign up</span></a>")); |
||||
|
ui->web_token_info_link->setOpenExternalLinks(true); |
||||
|
ui->web_token_info_link->setText( |
||||
|
tr("<a href='https://citra-emu.org/wiki/citra-web-service/'><span style=\"text-decoration: " |
||||
|
"underline; color:#039be5;\">What is my token?</span></a>")); |
||||
|
|
||||
|
ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry); |
||||
|
ui->edit_username->setText(QString::fromStdString(Settings::values.yuzu_username)); |
||||
|
ui->edit_token->setText(QString::fromStdString(Settings::values.yuzu_token)); |
||||
|
// Connect after setting the values, to avoid calling OnLoginChanged now
|
||||
|
connect(ui->edit_token, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged); |
||||
|
connect(ui->edit_username, &QLineEdit::textChanged, this, &ConfigureWeb::OnLoginChanged); |
||||
|
ui->label_telemetry_id->setText( |
||||
|
tr("Telemetry ID: 0x%1").arg(QString::number(Core::GetTelemetryId(), 16).toUpper())); |
||||
|
user_verified = true; |
||||
|
|
||||
|
ui->toggle_discordrpc->setChecked(UISettings::values.enable_discord_presence); |
||||
|
} |
||||
|
|
||||
|
void ConfigureWeb::applyConfiguration() { |
||||
|
Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked(); |
||||
|
UISettings::values.enable_discord_presence = ui->toggle_discordrpc->isChecked(); |
||||
|
if (user_verified) { |
||||
|
Settings::values.yuzu_username = ui->edit_username->text().toStdString(); |
||||
|
Settings::values.yuzu_token = ui->edit_token->text().toStdString(); |
||||
|
} else { |
||||
|
QMessageBox::warning(this, tr("Username and token not verified"), |
||||
|
tr("Username and token were not verified. The changes to your " |
||||
|
"username and/or token have not been saved.")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void ConfigureWeb::RefreshTelemetryID() { |
||||
|
const u64 new_telemetry_id{Core::RegenerateTelemetryId()}; |
||||
|
ui->label_telemetry_id->setText( |
||||
|
tr("Telemetry ID: 0x%1").arg(QString::number(new_telemetry_id, 16).toUpper())); |
||||
|
} |
||||
|
|
||||
|
void ConfigureWeb::OnLoginChanged() { |
||||
|
if (ui->edit_username->text().isEmpty() && ui->edit_token->text().isEmpty()) { |
||||
|
user_verified = true; |
||||
|
ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); |
||||
|
ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); |
||||
|
} else { |
||||
|
user_verified = false; |
||||
|
ui->label_username_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); |
||||
|
ui->label_token_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void ConfigureWeb::VerifyLogin() { |
||||
|
ui->button_verify_login->setDisabled(true); |
||||
|
ui->button_verify_login->setText(tr("Verifying")); |
||||
|
verify_watcher.setFuture( |
||||
|
QtConcurrent::run([this, username = ui->edit_username->text().toStdString(), |
||||
|
token = ui->edit_token->text().toStdString()]() { |
||||
|
return Core::VerifyLogin(username, token); |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
void ConfigureWeb::OnLoginVerified() { |
||||
|
ui->button_verify_login->setEnabled(true); |
||||
|
ui->button_verify_login->setText(tr("Verify")); |
||||
|
if (verify_watcher.result()) { |
||||
|
user_verified = true; |
||||
|
ui->label_username_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); |
||||
|
ui->label_token_verified->setPixmap(QIcon::fromTheme("checked").pixmap(16)); |
||||
|
} else { |
||||
|
ui->label_username_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); |
||||
|
ui->label_token_verified->setPixmap(QIcon::fromTheme("failed").pixmap(16)); |
||||
|
QMessageBox::critical( |
||||
|
this, tr("Verification failed"), |
||||
|
tr("Verification failed. Check that you have entered your username and token " |
||||
|
"correctly, and that your internet connection is working.")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void ConfigureWeb::retranslateUi() { |
||||
|
ui->retranslateUi(this); |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
// Copyright 2017 Citra Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <memory> |
||||
|
#include <QFutureWatcher> |
||||
|
#include <QWidget> |
||||
|
|
||||
|
namespace Ui { |
||||
|
class ConfigureWeb; |
||||
|
} |
||||
|
|
||||
|
class ConfigureWeb : public QWidget { |
||||
|
Q_OBJECT |
||||
|
|
||||
|
public: |
||||
|
explicit ConfigureWeb(QWidget* parent = nullptr); |
||||
|
~ConfigureWeb(); |
||||
|
|
||||
|
void applyConfiguration(); |
||||
|
void retranslateUi(); |
||||
|
|
||||
|
public slots: |
||||
|
void RefreshTelemetryID(); |
||||
|
void OnLoginChanged(); |
||||
|
void VerifyLogin(); |
||||
|
void OnLoginVerified(); |
||||
|
|
||||
|
private: |
||||
|
void setConfiguration(); |
||||
|
|
||||
|
bool user_verified = true; |
||||
|
QFutureWatcher<bool> verify_watcher; |
||||
|
|
||||
|
std::unique_ptr<Ui::ConfigureWeb> ui; |
||||
|
}; |
||||
@ -0,0 +1,206 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<ui version="4.0"> |
||||
|
<class>ConfigureWeb</class> |
||||
|
<widget class="QWidget" name="ConfigureWeb"> |
||||
|
<property name="geometry"> |
||||
|
<rect> |
||||
|
<x>0</x> |
||||
|
<y>0</y> |
||||
|
<width>926</width> |
||||
|
<height>561</height> |
||||
|
</rect> |
||||
|
</property> |
||||
|
<property name="windowTitle"> |
||||
|
<string>Form</string> |
||||
|
</property> |
||||
|
<layout class="QVBoxLayout" name="verticalLayout"> |
||||
|
<item> |
||||
|
<layout class="QVBoxLayout" name="verticalLayout_3"> |
||||
|
<item> |
||||
|
<widget class="QGroupBox" name="groupBoxWebConfig"> |
||||
|
<property name="title"> |
||||
|
<string>yuzu Web Service</string> |
||||
|
</property> |
||||
|
<layout class="QVBoxLayout" name="verticalLayoutYuzuWebService"> |
||||
|
<item> |
||||
|
<widget class="QLabel" name="web_credentials_disclaimer"> |
||||
|
<property name="text"> |
||||
|
<string>By providing your username and token, you agree to allow yuzu to collect additional usage data, which may include user identifying information.</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<layout class="QGridLayout" name="gridLayoutYuzuUsername"> |
||||
|
<item row="2" column="3"> |
||||
|
<widget class="QPushButton" name="button_verify_login"> |
||||
|
<property name="sizePolicy"> |
||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
||||
|
<horstretch>0</horstretch> |
||||
|
<verstretch>0</verstretch> |
||||
|
</sizepolicy> |
||||
|
</property> |
||||
|
<property name="layoutDirection"> |
||||
|
<enum>Qt::RightToLeft</enum> |
||||
|
</property> |
||||
|
<property name="text"> |
||||
|
<string>Verify</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="2" column="0"> |
||||
|
<widget class="QLabel" name="web_signup_link"> |
||||
|
<property name="text"> |
||||
|
<string>Sign up</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="0" column="1" colspan="3"> |
||||
|
<widget class="QLineEdit" name="edit_username"> |
||||
|
<property name="maxLength"> |
||||
|
<number>36</number> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="1" column="0"> |
||||
|
<widget class="QLabel" name="label_token"> |
||||
|
<property name="text"> |
||||
|
<string>Token: </string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="1" column="4"> |
||||
|
<widget class="QLabel" name="label_token_verified"> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="0" column="0"> |
||||
|
<widget class="QLabel" name="label_username"> |
||||
|
<property name="text"> |
||||
|
<string>Username: </string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="0" column="4"> |
||||
|
<widget class="QLabel" name="label_username_verified"> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="1" column="1" colspan="3"> |
||||
|
<widget class="QLineEdit" name="edit_token"> |
||||
|
<property name="maxLength"> |
||||
|
<number>36</number> |
||||
|
</property> |
||||
|
<property name="echoMode"> |
||||
|
<enum>QLineEdit::Password</enum> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="2" column="1"> |
||||
|
<widget class="QLabel" name="web_token_info_link"> |
||||
|
<property name="text"> |
||||
|
<string>What is my token?</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="2" column="2"> |
||||
|
<spacer name="horizontalSpacer"> |
||||
|
<property name="orientation"> |
||||
|
<enum>Qt::Horizontal</enum> |
||||
|
</property> |
||||
|
<property name="sizeHint" stdset="0"> |
||||
|
<size> |
||||
|
<width>40</width> |
||||
|
<height>20</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</spacer> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QGroupBox" name="groupBox"> |
||||
|
<property name="title"> |
||||
|
<string>Telemetry</string> |
||||
|
</property> |
||||
|
<layout class="QVBoxLayout" name="verticalLayout_2"> |
||||
|
<item> |
||||
|
<widget class="QCheckBox" name="toggle_telemetry"> |
||||
|
<property name="text"> |
||||
|
<string>Share anonymous usage data with the yuzu team</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QLabel" name="telemetry_learn_more"> |
||||
|
<property name="text"> |
||||
|
<string>Learn more</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<layout class="QGridLayout" name="gridLayoutTelemetryId"> |
||||
|
<item row="0" column="0"> |
||||
|
<widget class="QLabel" name="label_telemetry_id"> |
||||
|
<property name="text"> |
||||
|
<string>Telemetry ID:</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="0" column="1"> |
||||
|
<widget class="QPushButton" name="button_regenerate_telemetry_id"> |
||||
|
<property name="sizePolicy"> |
||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
||||
|
<horstretch>0</horstretch> |
||||
|
<verstretch>0</verstretch> |
||||
|
</sizepolicy> |
||||
|
</property> |
||||
|
<property name="layoutDirection"> |
||||
|
<enum>Qt::RightToLeft</enum> |
||||
|
</property> |
||||
|
<property name="text"> |
||||
|
<string>Regenerate</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</widget> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QGroupBox" name="discord_group"> |
||||
|
<property name="title"> |
||||
|
<string>Discord Presence</string> |
||||
|
</property> |
||||
|
<layout class="QVBoxLayout" name="verticalLayout_21"> |
||||
|
<item> |
||||
|
<widget class="QCheckBox" name="toggle_discordrpc"> |
||||
|
<property name="text"> |
||||
|
<string>Show Current Game in your Discord Status</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<spacer name="verticalSpacer"> |
||||
|
<property name="orientation"> |
||||
|
<enum>Qt::Vertical</enum> |
||||
|
</property> |
||||
|
<property name="sizeHint" stdset="0"> |
||||
|
<size> |
||||
|
<width>20</width> |
||||
|
<height>40</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</spacer> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</widget> |
||||
|
<resources/> |
||||
|
<connections/> |
||||
|
</ui> |
||||
@ -0,0 +1,25 @@ |
|||||
|
// Copyright 2018 Citra Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
namespace DiscordRPC { |
||||
|
|
||||
|
class DiscordInterface { |
||||
|
public: |
||||
|
virtual ~DiscordInterface() = default; |
||||
|
|
||||
|
virtual void Pause() = 0; |
||||
|
virtual void Update() = 0; |
||||
|
}; |
||||
|
|
||||
|
class NullImpl : public DiscordInterface { |
||||
|
public: |
||||
|
~NullImpl() = default; |
||||
|
|
||||
|
void Pause() override {} |
||||
|
void Update() override {} |
||||
|
}; |
||||
|
|
||||
|
} // namespace DiscordRPC |
||||
@ -0,0 +1,52 @@ |
|||||
|
// Copyright 2018 Citra Emulator Project
|
||||
|
// Licensed under GPLv2 or any later version
|
||||
|
// Refer to the license.txt file included.
|
||||
|
|
||||
|
#include <chrono>
|
||||
|
#include <string>
|
||||
|
#include <discord_rpc.h>
|
||||
|
#include "common/common_types.h"
|
||||
|
#include "core/core.h"
|
||||
|
#include "core/loader/loader.h"
|
||||
|
#include "yuzu/discord_impl.h"
|
||||
|
#include "yuzu/ui_settings.h"
|
||||
|
|
||||
|
namespace DiscordRPC { |
||||
|
|
||||
|
DiscordImpl::DiscordImpl() { |
||||
|
DiscordEventHandlers handlers{}; |
||||
|
|
||||
|
// The number is the client ID for yuzu, it's used for images and the
|
||||
|
// application name
|
||||
|
Discord_Initialize("471872241299226636", &handlers, 1, nullptr); |
||||
|
} |
||||
|
|
||||
|
DiscordImpl::~DiscordImpl() { |
||||
|
Discord_ClearPresence(); |
||||
|
Discord_Shutdown(); |
||||
|
} |
||||
|
|
||||
|
void DiscordImpl::Pause() { |
||||
|
Discord_ClearPresence(); |
||||
|
} |
||||
|
|
||||
|
void DiscordImpl::Update() { |
||||
|
s64 start_time = std::chrono::duration_cast<std::chrono::seconds>( |
||||
|
std::chrono::system_clock::now().time_since_epoch()) |
||||
|
.count(); |
||||
|
std::string title; |
||||
|
if (Core::System::GetInstance().IsPoweredOn()) |
||||
|
Core::System::GetInstance().GetAppLoader().ReadTitle(title); |
||||
|
DiscordRichPresence presence{}; |
||||
|
presence.largeImageKey = "yuzu_logo"; |
||||
|
presence.largeImageText = "yuzu is an emulator for the Nintendo Switch"; |
||||
|
if (Core::System::GetInstance().IsPoweredOn()) { |
||||
|
presence.state = title.c_str(); |
||||
|
presence.details = "Currently in game"; |
||||
|
} else { |
||||
|
presence.details = "Not in game"; |
||||
|
} |
||||
|
presence.startTimestamp = start_time; |
||||
|
Discord_UpdatePresence(&presence); |
||||
|
} |
||||
|
} // namespace DiscordRPC
|
||||
@ -0,0 +1,20 @@ |
|||||
|
// Copyright 2018 Citra Emulator Project |
||||
|
// Licensed under GPLv2 or any later version |
||||
|
// Refer to the license.txt file included. |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include "yuzu/discord.h" |
||||
|
|
||||
|
namespace DiscordRPC { |
||||
|
|
||||
|
class DiscordImpl : public DiscordInterface { |
||||
|
public: |
||||
|
DiscordImpl(); |
||||
|
~DiscordImpl(); |
||||
|
|
||||
|
void Pause() override; |
||||
|
void Update() override; |
||||
|
}; |
||||
|
|
||||
|
} // namespace DiscordRPC |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue