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 |
|||
|
|||
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