mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-24 07:52:05 +08:00
Merge branch 'master-new' into nnlc-new
This commit is contained in:
@@ -1,13 +1,9 @@
|
||||
from openpilot.common.api.comma_connect import CommaConnectApi
|
||||
from sunnypilot.sunnylink.api import SunnylinkApi
|
||||
|
||||
|
||||
class Api:
|
||||
def __init__(self, dongle_id, use_sunnylink=False):
|
||||
if use_sunnylink:
|
||||
self.service = SunnylinkApi(dongle_id)
|
||||
else:
|
||||
self.service = CommaConnectApi(dongle_id)
|
||||
def __init__(self, dongle_id):
|
||||
self.service = CommaConnectApi(dongle_id)
|
||||
|
||||
def request(self, method, endpoint, **params):
|
||||
return self.service.request(method, endpoint, **params)
|
||||
@@ -22,6 +18,5 @@ class Api:
|
||||
return self.service.get_token(expiry_hours)
|
||||
|
||||
|
||||
def api_get(endpoint, method='GET', timeout=None, access_token=None, use_sunnylink=False, **params):
|
||||
return SunnylinkApi(None).api_get(endpoint, method, timeout, access_token, **params) if use_sunnylink \
|
||||
else CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params)
|
||||
def api_get(endpoint, method='GET', timeout=None, access_token=None, **params):
|
||||
return CommaConnectApi(None).api_get(endpoint, method, timeout, access_token, **params)
|
||||
|
||||
@@ -148,6 +148,8 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
|
||||
// sunnylink params
|
||||
{"EnableSunnylinkUploader", PERSISTENT | BACKUP},
|
||||
{"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START},
|
||||
{"SunnylinkCache_Roles", PERSISTENT},
|
||||
{"SunnylinkCache_Users", PERSISTENT},
|
||||
{"SunnylinkDongleId", PERSISTENT},
|
||||
{"SunnylinkdPid", PERSISTENT},
|
||||
{"SunnylinkEnabled", PERSISTENT},
|
||||
|
||||
+15
-10
@@ -79,31 +79,36 @@ bool HttpRequest::timeout() const {
|
||||
return reply && reply->error() == QNetworkReply::OperationCanceledError;
|
||||
}
|
||||
|
||||
void HttpRequest::sendRequest(const QString &requestURL, const HttpRequest::Method method) {
|
||||
if (active()) {
|
||||
qDebug() << "HttpRequest is active";
|
||||
return;
|
||||
}
|
||||
QNetworkRequest HttpRequest::prepareRequest(const QString &requestURL) {
|
||||
QNetworkRequest request;
|
||||
QString token;
|
||||
if (create_jwt) {
|
||||
token = CommaApi::create_jwt();
|
||||
token = GetJwtToken();
|
||||
} else {
|
||||
QString token_json = QString::fromStdString(util::read_file(util::getenv("HOME") + "/.comma/auth.json"));
|
||||
QJsonDocument json_d = QJsonDocument::fromJson(token_json.toUtf8());
|
||||
token = json_d["access_token"].toString();
|
||||
}
|
||||
|
||||
QNetworkRequest request;
|
||||
request.setUrl(QUrl(requestURL));
|
||||
request.setRawHeader("User-Agent", getUserAgent().toUtf8());
|
||||
request.setRawHeader("User-Agent", GetUserAgent().toUtf8());
|
||||
|
||||
if (!token.isEmpty()) {
|
||||
request.setRawHeader(QByteArray("Authorization"), ("JWT " + token).toUtf8());
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
if (method == HttpRequest::Method::GET) {
|
||||
void HttpRequest::sendRequest(const QString &requestURL, const Method method) {
|
||||
if (active()) {
|
||||
qDebug() << "HttpRequest is active";
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request = prepareRequest(requestURL);
|
||||
if (method == Method::GET) {
|
||||
reply = nam()->get(request);
|
||||
} else if (method == HttpRequest::Method::DELETE) {
|
||||
} else if (method == Method::DELETE) {
|
||||
reply = nam()->deleteResource(request);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
#include "util.h"
|
||||
#include "common/util.h"
|
||||
|
||||
namespace CommaApi {
|
||||
@@ -23,10 +24,11 @@ class HttpRequest : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class Method {GET, DELETE};
|
||||
enum class Method {GET, DELETE, POST, PUT};
|
||||
|
||||
explicit HttpRequest(QObject* parent, bool create_jwt = true, int timeout = 20000);
|
||||
void sendRequest(const QString &requestURL, const Method method = Method::GET);
|
||||
virtual void sendRequest(const QString &requestURL, Method method);
|
||||
void sendRequest(const QString &requestURL) { sendRequest(requestURL, Method::GET);}
|
||||
bool active() const;
|
||||
bool timeout() const;
|
||||
|
||||
@@ -35,13 +37,14 @@ signals:
|
||||
|
||||
protected:
|
||||
QNetworkReply *reply = nullptr;
|
||||
|
||||
private:
|
||||
static QNetworkAccessManager *nam();
|
||||
QTimer *networkTimer = nullptr;
|
||||
bool create_jwt;
|
||||
virtual QNetworkRequest prepareRequest(const QString& requestURL);
|
||||
[[nodiscard]] virtual QString GetJwtToken() const { return CommaApi::create_jwt(); }
|
||||
[[nodiscard]] virtual QString GetUserAgent() const { return getUserAgent(); }
|
||||
|
||||
private slots:
|
||||
protected slots:
|
||||
void requestTimeout();
|
||||
void requestFinished();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
widgets_src = [
|
||||
"sunnypilot/qt/request_repeater.cc",
|
||||
"sunnypilot/qt/widgets/toggle.cc",
|
||||
"sunnypilot/qt/widgets/controls.cc",
|
||||
"sunnypilot/qt/widgets/drive_stats.cc",
|
||||
@@ -13,6 +14,7 @@ qt_util = [
|
||||
|
||||
qt_src = [
|
||||
"sunnypilot/ui.cc",
|
||||
"sunnypilot/qt/api.cc",
|
||||
"sunnypilot/qt/sidebar.cc",
|
||||
"sunnypilot/qt/window.cc",
|
||||
"sunnypilot/qt/home.cc",
|
||||
@@ -21,6 +23,7 @@ qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/settings.cc",
|
||||
"sunnypilot/qt/offroad/settings/software_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnylink_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.cc",
|
||||
"sunnypilot/qt/offroad/settings/sunnypilot_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/trips_panel.cc",
|
||||
"sunnypilot/qt/offroad/settings/vehicle_panel.cc",
|
||||
@@ -36,11 +39,18 @@ sunnypilot_panel_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/sunnypilot/neural_network_lateral_control.cc",
|
||||
]
|
||||
|
||||
network_src = [
|
||||
"sunnypilot/qt/network/sunnylink/sunnylink_client.cc",
|
||||
"sunnypilot/qt/network/sunnylink/services/base_device_service.cc",
|
||||
"sunnypilot/qt/network/sunnylink/services/role_service.cc",
|
||||
"sunnypilot/qt/network/sunnylink/services/user_service.cc",
|
||||
]
|
||||
|
||||
vehicle_panel_qt_src = [
|
||||
"sunnypilot/qt/offroad/settings/vehicle/platform_selector.cc",
|
||||
]
|
||||
|
||||
sp_widgets_src = widgets_src
|
||||
sp_widgets_src = widgets_src + network_src
|
||||
sp_qt_src = qt_src + sunnypilot_panel_qt_src + vehicle_panel_qt_src
|
||||
sp_qt_util = qt_util
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/api.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include "util.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
namespace SunnylinkApi {
|
||||
QString create_jwt(const QJsonObject &payloads, int expiry, bool sunnylink) {
|
||||
QJsonObject header = {{"alg", "RS256"}};
|
||||
|
||||
auto t = QDateTime::currentSecsSinceEpoch();
|
||||
auto dongle_id = sunnylink ? getSunnylinkDongleId() : getDongleId();
|
||||
QJsonObject payload = {{"identity", dongle_id.value_or("")}, {"nbf", t}, {"iat", t}, {"exp", t + expiry}};
|
||||
for (auto it = payloads.begin(); it != payloads.end(); ++it) {
|
||||
payload.insert(it.key(), it.value());
|
||||
}
|
||||
|
||||
auto b64_opts = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals;
|
||||
QString jwt = QJsonDocument(header).toJson(QJsonDocument::Compact).toBase64(b64_opts) + '.' +
|
||||
QJsonDocument(payload).toJson(QJsonDocument::Compact).toBase64(b64_opts);
|
||||
|
||||
auto hash = QCryptographicHash::hash(jwt.toUtf8(), QCryptographicHash::Sha256);
|
||||
return jwt + "." + CommaApi::rsa_sign(hash).toBase64(b64_opts);
|
||||
}
|
||||
} // namespace SunnylinkApi
|
||||
|
||||
void HttpRequestSP::sendRequest(const QString& requestURL, Method method, const QByteArray& payload) {
|
||||
if (active()) {
|
||||
return;
|
||||
}
|
||||
QNetworkRequest request = prepareRequest(requestURL);
|
||||
|
||||
if(!payload.isEmpty() && (method == Method::POST || method == Method::PUT)) {
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case Method::GET:
|
||||
reply = nam()->get(request);
|
||||
break;
|
||||
case Method::DELETE:
|
||||
reply = nam()->deleteResource(request);
|
||||
break;
|
||||
case Method::POST:
|
||||
reply = nam()->post(request, payload);
|
||||
break;
|
||||
case Method::PUT:
|
||||
reply = nam()->put(request, payload);
|
||||
break;
|
||||
}
|
||||
|
||||
networkTimer->start();
|
||||
connect(reply, &QNetworkReply::finished, this, &HttpRequestSP::requestFinished);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/qt/api.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/util.h"
|
||||
#include "common/util.h"
|
||||
|
||||
namespace SunnylinkApi {
|
||||
QByteArray rsa_encrypt(const QByteArray& data);
|
||||
QByteArray rsa_decrypt(const QByteArray& data);
|
||||
QString create_jwt(const QJsonObject& payloads = {}, int expiry = 3600, bool sunnylink = false);
|
||||
}
|
||||
|
||||
class HttpRequestSP : public HttpRequest {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit HttpRequestSP(QObject* parent, bool create_jwt = true, int timeout = 20000, bool sunnylink = false) :
|
||||
HttpRequest(parent, create_jwt, timeout), sunnylink(sunnylink) {}
|
||||
|
||||
using HttpRequest::sendRequest;
|
||||
void sendRequest(const QString& requestURL, Method method, const QByteArray& payload);
|
||||
|
||||
private:
|
||||
bool sunnylink;
|
||||
|
||||
protected:
|
||||
[[nodiscard]] QString GetJwtToken() const override { return SunnylinkApi::create_jwt({}, 3600, sunnylink); }
|
||||
[[nodiscard]] QString GetUserAgent() const override { return getUserAgent(sunnylink); }
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
enum class RoleType {
|
||||
ReadOnly,
|
||||
Sponsor,
|
||||
Admin
|
||||
};
|
||||
|
||||
// haha, a role model xD
|
||||
class RoleModel {
|
||||
protected:
|
||||
QJsonObject m_raw_json_object;
|
||||
|
||||
public:
|
||||
RoleType roleType;
|
||||
|
||||
explicit RoleModel(const RoleType &roleType) : roleType(roleType) { m_raw_json_object = toJson(); }
|
||||
explicit RoleModel(const QJsonObject &json) : RoleModel(stringToRoleType(json["role_type"].toString())) { m_raw_json_object = json; }
|
||||
|
||||
[[nodiscard]] QJsonObject toJson() const {
|
||||
QJsonObject json;
|
||||
json["role_type"] = roleTypeToString(roleType);
|
||||
return json;
|
||||
}
|
||||
|
||||
static RoleType stringToRoleType(const QString &roleTypeString) {
|
||||
if (roleTypeString == "ReadOnly") return RoleType::ReadOnly;
|
||||
if (roleTypeString == "Sponsor") return RoleType::Sponsor;
|
||||
|
||||
return RoleType::Admin; // Default to Admin
|
||||
}
|
||||
|
||||
static QString roleTypeToString(const RoleType &roleType) {
|
||||
switch (roleType) {
|
||||
case RoleType::ReadOnly:
|
||||
return "ReadOnly";
|
||||
case RoleType::Sponsor:
|
||||
return "Sponsor";
|
||||
default: // RoleType::Admin
|
||||
return "Admin";
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, typename = typename std::enable_if<std::is_base_of<RoleModel, T>::value>::type> T as() const {
|
||||
return T(m_raw_json_object);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
enum class SponsorTier {
|
||||
Free,
|
||||
Novice,
|
||||
Supporter,
|
||||
Contributor,
|
||||
Benefactor,
|
||||
Guardian,
|
||||
};
|
||||
|
||||
// haha, a role model xD
|
||||
class SponsorRoleModel final : RoleModel {
|
||||
public:
|
||||
SponsorTier roleTier;
|
||||
|
||||
explicit SponsorRoleModel(const RoleType &roleType, const SponsorTier &roleTier) : RoleModel(roleType), roleTier(roleTier) {}
|
||||
explicit SponsorRoleModel(const QJsonObject &json) : RoleModel(json), roleTier(stringToSponsorTier(json["role_tier"].toString())) {}
|
||||
|
||||
[[nodiscard]] QJsonObject toJson() const {
|
||||
QJsonObject json = RoleModel::toJson();
|
||||
json["role_tier"] = sponsorTierToString(roleTier);
|
||||
return json;
|
||||
}
|
||||
|
||||
static SponsorTier stringToSponsorTier(const QString &sponsorTierString) {
|
||||
const auto sponsorTierStringLower = sponsorTierString.toLower();
|
||||
if (sponsorTierStringLower == "guardian") return SponsorTier::Guardian;
|
||||
if (sponsorTierStringLower == "novice") return SponsorTier::Novice;
|
||||
if (sponsorTierStringLower == "supporter") return SponsorTier::Supporter;
|
||||
if (sponsorTierStringLower == "contributor") return SponsorTier::Contributor;
|
||||
if (sponsorTierStringLower == "benefactor") return SponsorTier::Benefactor;
|
||||
|
||||
// Default to Free
|
||||
return SponsorTier::Free;
|
||||
}
|
||||
|
||||
static QString sponsorTierToString(const SponsorTier &sponsorTier) {
|
||||
switch (sponsorTier) {
|
||||
case SponsorTier::Guardian:
|
||||
return "Guardian";
|
||||
case SponsorTier::Novice:
|
||||
return "Novice";
|
||||
case SponsorTier::Supporter:
|
||||
return "Supporter";
|
||||
case SponsorTier::Contributor:
|
||||
return "Contributor";
|
||||
case SponsorTier::Benefactor:
|
||||
return "Benefactor";
|
||||
|
||||
default: // SponsorTier::Free
|
||||
return "Free";
|
||||
}
|
||||
}
|
||||
[[nodiscard]] auto getSponsorTierString() const { return sponsorTierToString(roleTier); }
|
||||
|
||||
static QString sponsorTierToColor(const SponsorTier &sponsorTier) {
|
||||
switch (sponsorTier) {
|
||||
case SponsorTier::Guardian:
|
||||
return "gold";
|
||||
case SponsorTier::Benefactor:
|
||||
return "mediumseagreen";
|
||||
case SponsorTier::Contributor:
|
||||
return "steelblue";
|
||||
case SponsorTier::Supporter:
|
||||
return "mediumpurple";
|
||||
case SponsorTier::Novice:
|
||||
return "white";
|
||||
default: // SponsorTier::Free
|
||||
return "silver";
|
||||
}
|
||||
}
|
||||
[[nodiscard]] auto getSponsorTierColor() const { return sponsorTierToColor(roleTier); }
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
class UserModel {
|
||||
public:
|
||||
QString device_id;
|
||||
QString user_id;
|
||||
qint64 created_at;
|
||||
qint64 updated_at;
|
||||
QString token_hash;
|
||||
|
||||
explicit UserModel(const QJsonObject &json) {
|
||||
device_id = json["device_id"].toString();
|
||||
user_id = json["user_id"].toString();
|
||||
created_at = json["created_at"].toInt();
|
||||
updated_at = json["updated_at"].toInt();
|
||||
token_hash = json["token_hash"].toString();
|
||||
}
|
||||
|
||||
[[nodiscard]] QJsonObject toJson() const {
|
||||
QJsonObject json;
|
||||
json["device_id"] = device_id;
|
||||
json["user_id"] = user_id;
|
||||
json["created_at"] = created_at;
|
||||
json["updated_at"] = updated_at;
|
||||
json["token_hash"] = token_hash;
|
||||
return json;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink//services/base_device_service.h"
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/request_repeater.h"
|
||||
|
||||
#include "common/swaglog.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h"
|
||||
|
||||
BaseDeviceService::BaseDeviceService(QObject* parent) : QObject(parent) {
|
||||
param_watcher = new ParamWatcher(this);
|
||||
connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) {
|
||||
paramsRefresh();
|
||||
});
|
||||
param_watcher->addParam("SunnylinkEnabled");
|
||||
}
|
||||
|
||||
void BaseDeviceService::paramsRefresh() {
|
||||
}
|
||||
|
||||
void BaseDeviceService::loadDeviceData(const QString &url, bool poll) {
|
||||
if (!is_sunnylink_enabled()) {
|
||||
LOGW("Sunnylink is not enabled, refusing to load data.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto sl_dongle_id = getSunnylinkDongleId();
|
||||
if (!sl_dongle_id.has_value())
|
||||
return;
|
||||
|
||||
QString fullUrl = SUNNYLINK_BASE_URL + "/device/" + *sl_dongle_id + url;
|
||||
if (poll && !isCurrentyPolling()) {
|
||||
LOGD("Polling %s", qPrintable(fullUrl));
|
||||
LOGD("Cache key: SunnylinkCache_%s", qPrintable(QString(getCacheKey())));
|
||||
repeater = new RequestRepeaterSP(this, fullUrl, "SunnylinkCache_" + getCacheKey(), 60, false, true);
|
||||
connect(repeater, &RequestRepeaterSP::requestDone, this, &BaseDeviceService::handleResponse);
|
||||
} else if (isCurrentyPolling()) {
|
||||
repeater->ForceUpdate();
|
||||
} else {
|
||||
LOGD("Sending one-time %s", qPrintable(fullUrl));
|
||||
initial_request = new HttpRequestSP(this, true, 10000, true);
|
||||
connect(initial_request, &HttpRequestSP::requestDone, this, &BaseDeviceService::handleResponse);
|
||||
}
|
||||
}
|
||||
|
||||
void BaseDeviceService::stopPolling() {
|
||||
if (repeater != nullptr) {
|
||||
repeater->deleteLater();
|
||||
repeater = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/request_repeater.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
|
||||
class BaseDeviceService : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
protected:
|
||||
void paramsRefresh();
|
||||
void loadDeviceData(const QString &url, bool poll = false);
|
||||
virtual void handleResponse(const QString &response, bool success) = 0;
|
||||
|
||||
static bool is_sunnylink_enabled() { return Params().getBool("SunnylinkEnabled");}
|
||||
ParamWatcher* param_watcher;
|
||||
HttpRequestSP* initial_request = nullptr;
|
||||
RequestRepeaterSP* repeater = nullptr;
|
||||
|
||||
public:
|
||||
explicit BaseDeviceService(QObject* parent = nullptr);
|
||||
virtual QString getCacheKey() const = 0;
|
||||
bool isCurrentyPolling() {return repeater != nullptr;}
|
||||
void stopPolling();
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
RoleService::RoleService(QObject* parent) : BaseDeviceService(parent) {}
|
||||
|
||||
void RoleService::load() {
|
||||
loadDeviceData(url);
|
||||
}
|
||||
|
||||
void RoleService::startPolling() {
|
||||
loadDeviceData(url, true);
|
||||
}
|
||||
|
||||
void RoleService::handleResponse(const QString &response, bool success) {
|
||||
if (!success) return;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
QJsonArray jsonArray = doc.array();
|
||||
|
||||
std::vector<RoleModel> roles;
|
||||
for (const auto &value : jsonArray) {
|
||||
roles.emplace_back(value.toObject());
|
||||
}
|
||||
|
||||
emit rolesReady(roles);
|
||||
uiStateSP()->setSunnylinkRoles(roles);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h"
|
||||
|
||||
class RoleService : public BaseDeviceService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RoleService(QObject* parent = nullptr);
|
||||
void load();
|
||||
void startPolling();
|
||||
[[nodiscard]] QString getCacheKey() const final { return "Roles"; }
|
||||
|
||||
signals:
|
||||
void rolesReady(const std::vector<RoleModel> &roles);
|
||||
|
||||
protected:
|
||||
void handleResponse(const QString&response, bool success) override;
|
||||
|
||||
private:
|
||||
QString url = "/roles";
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
|
||||
UserService::UserService(QObject* parent) : BaseDeviceService(parent) {
|
||||
url = "/users";
|
||||
}
|
||||
|
||||
void UserService::load() {
|
||||
loadDeviceData(url);
|
||||
}
|
||||
|
||||
void UserService::startPolling() {
|
||||
loadDeviceData(url, true);
|
||||
}
|
||||
|
||||
void UserService::handleResponse(const QString &response, bool success) {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
QJsonArray jsonArray = doc.array();
|
||||
|
||||
std::vector<UserModel> users;
|
||||
for (const auto &value : jsonArray) {
|
||||
users.emplace_back(value.toObject());
|
||||
}
|
||||
|
||||
emit usersReady(users);
|
||||
uiStateSP()->setSunnylinkDeviceUsers(users);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/base_device_service.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h"
|
||||
|
||||
class UserService : public BaseDeviceService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UserService(QObject* parent = nullptr);
|
||||
void load();
|
||||
void startPolling();
|
||||
[[nodiscard]] QString getCacheKey() const final { return "Users"; };
|
||||
|
||||
signals:
|
||||
void usersReady(const std::vector<UserModel>&users);
|
||||
|
||||
protected:
|
||||
void handleResponse(const QString&response, bool success) override;
|
||||
|
||||
private:
|
||||
QString url = "/users";
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h"
|
||||
|
||||
SunnylinkClient::SunnylinkClient(QObject* parent) : QObject(parent) {
|
||||
role_service = new RoleService(parent);
|
||||
user_service = new UserService(parent);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/role_service.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/services/user_service.h"
|
||||
|
||||
class SunnylinkClient : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SunnylinkClient(QObject* parent);
|
||||
RoleService* role_service;
|
||||
UserService* user_service;
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h"
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/api.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h"
|
||||
|
||||
// Sponsor Upsell
|
||||
using qrcodegen::QrCode;
|
||||
|
||||
SunnylinkSponsorQRWidget::SunnylinkSponsorQRWidget(bool sponsor_pair, QWidget* parent) : QWidget(parent), sponsor_pair(sponsor_pair) {
|
||||
timer = new QTimer(this);
|
||||
connect(timer, &QTimer::timeout, this, &SunnylinkSponsorQRWidget::refresh);
|
||||
}
|
||||
|
||||
void SunnylinkSponsorQRWidget::showEvent(QShowEvent *event) {
|
||||
refresh();
|
||||
timer->start(5 * 60 * 1000);
|
||||
device()->setOffroadBrightness(100);
|
||||
}
|
||||
|
||||
void SunnylinkSponsorQRWidget::hideEvent(QHideEvent *event) {
|
||||
timer->stop();
|
||||
device()->setOffroadBrightness(BACKLIGHT_OFFROAD);
|
||||
}
|
||||
|
||||
void SunnylinkSponsorQRWidget::refresh() {
|
||||
QString qrString;
|
||||
|
||||
if (sponsor_pair) {
|
||||
QString token = SunnylinkApi::create_jwt({}, 3600, true);
|
||||
auto sl_dongle_id = getSunnylinkDongleId();
|
||||
QByteArray payload = QString("1|" + sl_dongle_id.value_or("") + "|" + token).toUtf8().toBase64();
|
||||
qrString = SUNNYLINK_BASE_URL + "/sso?state=" + payload;
|
||||
} else {
|
||||
qrString = "https://github.com/sponsors/sunnyhaibin";
|
||||
}
|
||||
|
||||
this->updateQrCode(qrString);
|
||||
update();
|
||||
}
|
||||
|
||||
void SunnylinkSponsorQRWidget::updateQrCode(const QString &text) {
|
||||
QrCode qr = QrCode::encodeText(text.toUtf8().data(), QrCode::Ecc::LOW);
|
||||
qint32 sz = qr.getSize();
|
||||
QImage im(sz, sz, QImage::Format_RGB32);
|
||||
|
||||
QRgb black = qRgb(0, 0, 0);
|
||||
QRgb white = qRgb(255, 255, 255);
|
||||
for (int y = 0; y < sz; y++) {
|
||||
for (int x = 0; x < sz; x++) {
|
||||
im.setPixel(x, y, qr.getModule(x, y) ? black : white);
|
||||
}
|
||||
}
|
||||
|
||||
// Integer division to prevent anti-aliasing
|
||||
int final_sz = ((width() / sz) - 1) * sz;
|
||||
img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly);
|
||||
}
|
||||
|
||||
void SunnylinkSponsorQRWidget::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), Qt::white);
|
||||
|
||||
QSize s = (size() - img.size()) / 2;
|
||||
p.drawPixmap(s.width(), s.height(), img);
|
||||
}
|
||||
|
||||
QStringList SunnylinkSponsorPopup::getInstructions(bool sponsor_pair) {
|
||||
QStringList instructions;
|
||||
if (sponsor_pair) {
|
||||
instructions << tr("Scan the QR code to login to your GitHub account")
|
||||
<< tr("Follow the prompts to complete the pairing process")
|
||||
<< tr("Re-enter the \"sunnylink\" panel to verify sponsorship status")
|
||||
<< tr("If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot");
|
||||
} else {
|
||||
instructions << tr("Scan the QR code to visit sunnyhaibin's GitHub Sponsors page")
|
||||
<< tr("Choose your sponsorship tier and confirm your support")
|
||||
<< tr("Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status");
|
||||
}
|
||||
return instructions;
|
||||
}
|
||||
|
||||
SunnylinkSponsorPopup::SunnylinkSponsorPopup(bool sponsor_pair, QWidget *parent) : DialogBase(parent), sponsor_pair(sponsor_pair) {
|
||||
auto *hlayout = new QHBoxLayout(this);
|
||||
auto sunnylink_client = new SunnylinkClient(this);
|
||||
hlayout->setContentsMargins(0, 0, 0, 0);
|
||||
hlayout->setSpacing(0);
|
||||
|
||||
setStyleSheet("SunnylinkSponsorPopup { background-color: #E0E0E0; }");
|
||||
|
||||
// text
|
||||
auto vlayout = new QVBoxLayout();
|
||||
vlayout->setContentsMargins(85, 70, 50, 70);
|
||||
vlayout->setSpacing(50);
|
||||
hlayout->addLayout(vlayout, 1);
|
||||
{
|
||||
auto close = new QPushButton(QIcon(":/icons/close.svg"), "", this);
|
||||
close->setIconSize(QSize(80, 80));
|
||||
close->setStyleSheet("border: none;");
|
||||
vlayout->addWidget(close, 0, Qt::AlignLeft);
|
||||
connect(close, &QPushButton::clicked, this, [=] {
|
||||
sunnylink_client->role_service->load();
|
||||
sunnylink_client->user_service->load();
|
||||
QDialog::reject();
|
||||
});
|
||||
|
||||
//vlayout->addSpacing(30);
|
||||
|
||||
const QString titleText = sponsor_pair ? tr("Pair your GitHub account") : tr("Early Access: Become a sunnypilot Sponsor");
|
||||
const auto title = new QLabel(titleText, this);
|
||||
title->setStyleSheet("font-size: 75px; color: black;");
|
||||
title->setWordWrap(true);
|
||||
vlayout->addWidget(title);
|
||||
|
||||
QStringList instructions = getInstructions(sponsor_pair);
|
||||
QString instructionsHtml = "<ol type='1' style='margin-left: 15px;'>";
|
||||
for (const auto & instruction : instructions) {
|
||||
instructionsHtml += QString("<li style='margin-bottom: 50px;'>%1</li>").arg(instruction);
|
||||
}
|
||||
instructionsHtml += "</ol>";
|
||||
const auto instructionsLabel = new QLabel(instructionsHtml, this);
|
||||
|
||||
|
||||
instructionsLabel->setStyleSheet("font-size: 47px; font-weight: bold; color: black;");
|
||||
instructionsLabel->setWordWrap(true);
|
||||
vlayout->addWidget(instructionsLabel);
|
||||
|
||||
vlayout->addStretch();
|
||||
}
|
||||
|
||||
// QR code
|
||||
auto *qr = new SunnylinkSponsorQRWidget(sponsor_pair, this);
|
||||
hlayout->addWidget(qr, 1);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QrCode.hpp>
|
||||
#include <QtCore/qjsonobject.h>
|
||||
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h"
|
||||
|
||||
const QString SUNNYLINK_BASE_URL = util::getenv("SUNNYLINK_API_HOST", "https://stg.api.sunnypilot.ai").c_str();
|
||||
|
||||
class SunnylinkSponsorQRWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SunnylinkSponsorQRWidget(bool sponsor_pair = false, QWidget* parent = 0);
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
||||
private:
|
||||
QPixmap img;
|
||||
QTimer *timer;
|
||||
void updateQrCode(const QString &text);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
|
||||
bool sponsor_pair = false;
|
||||
|
||||
private slots:
|
||||
void refresh();
|
||||
};
|
||||
|
||||
// sponsor popup widget
|
||||
class SunnylinkSponsorPopup : public DialogBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SunnylinkSponsorPopup(bool sponsor_pair = false, QWidget* parent = 0);
|
||||
|
||||
private:
|
||||
static QStringList getInstructions(bool sponsor_pair);
|
||||
bool sponsor_pair = false;
|
||||
};
|
||||
@@ -12,12 +12,26 @@
|
||||
|
||||
SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
|
||||
main_layout = new QStackedLayout(this);
|
||||
ListWidget *list = new ListWidget(this, false);
|
||||
sunnylink_client = new SunnylinkClient(this);
|
||||
param_watcher = new ParamWatcher(this);
|
||||
param_watcher->addParam("SunnylinkEnabled");
|
||||
connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) {
|
||||
paramsRefresh(param_name, param_value);
|
||||
});
|
||||
|
||||
is_sunnylink_enabled = Params().getBool("SunnylinkEnabled");
|
||||
connect(uiStateSP(), &UIStateSP::sunnylinkRolesChanged, this, &SunnylinkPanel::updatePanel);
|
||||
connect(uiStateSP(), &UIStateSP::sunnylinkDeviceUsersChanged, this, &SunnylinkPanel::updatePanel);
|
||||
connect(uiStateSP(), &UIStateSP::offroadTransition, [=](bool offroad) {
|
||||
is_onroad = !offroad;
|
||||
updatePanel();
|
||||
});
|
||||
|
||||
sunnylinkScreen = new QWidget(this);
|
||||
QVBoxLayout *vlayout = new QVBoxLayout(sunnylinkScreen);
|
||||
auto vlayout = new QVBoxLayout(sunnylinkScreen);
|
||||
vlayout->setContentsMargins(50, 20, 50, 20);
|
||||
|
||||
auto *list = new ListWidget(this, false);
|
||||
QString sunnylinkEnabledBtnDesc = tr("This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that.");
|
||||
sunnylinkEnabledBtn = new ParamControl(
|
||||
"SunnylinkEnabled",
|
||||
@@ -26,23 +40,47 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
|
||||
"");
|
||||
list->addItem(sunnylinkEnabledBtn);
|
||||
|
||||
status_popup = new SunnylinkSponsorPopup(false, this);
|
||||
sponsorBtn = new ButtonControlSP(
|
||||
tr("Sponsor Status"), tr("SPONSOR"),
|
||||
tr("Become a sponsor of sunnypilot to get early access to sunnylink features when they become available."));
|
||||
list->addItem(sponsorBtn);
|
||||
connect(sponsorBtn, &ButtonControlSP::clicked, [=]() {
|
||||
status_popup->exec();
|
||||
});
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
pair_popup = new SunnylinkSponsorPopup(true, this);
|
||||
pairSponsorBtn = new ButtonControlSP(
|
||||
tr("Pair GitHub Account"), tr("PAIR"),
|
||||
tr("Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.") + "🌟");
|
||||
list->addItem(pairSponsorBtn);
|
||||
connect(pairSponsorBtn, &ButtonControlSP::clicked, [=]() {
|
||||
if (getSunnylinkDongleId().value_or(tr("N/A")) == "N/A") {
|
||||
ConfirmationDialog::alert(tr("sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again."), this);
|
||||
} else {
|
||||
pair_popup->exec();
|
||||
}
|
||||
});
|
||||
list->addItem(horizontal_line());
|
||||
|
||||
connect(sunnylinkEnabledBtn, &ParamControl::showDescriptionEvent, [=]() {
|
||||
// resets the description to the default one for the Easter egg
|
||||
sunnylinkEnabledBtn->setDescription(sunnylinkEnabledBtnDesc);
|
||||
});
|
||||
|
||||
connect(sunnylinkEnabledBtn, &ParamControl::toggleFlipped, [=](bool enabled) {
|
||||
QString description;
|
||||
if (enabled) {
|
||||
auto proud_description = "<font color='SeaGreen'>"+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ "</font>";
|
||||
sunnylinkEnabledBtn->showDescription();
|
||||
sunnylinkEnabledBtn->setDescription(proud_description);
|
||||
description = "<font color='SeaGreen'>"+ tr("🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀")+ "</font>";
|
||||
} else {
|
||||
auto shame_description = "<font color='orange'>"+ tr("👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉.")+ "</font>";
|
||||
sunnylinkEnabledBtn->showDescription();
|
||||
sunnylinkEnabledBtn->setDescription(shame_description);
|
||||
}
|
||||
description = "<font color='orange'>"+ tr("👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉.")+ "</font>";
|
||||
|
||||
updatePanel(offroad);
|
||||
}
|
||||
sunnylinkEnabledBtn->showDescription();
|
||||
sunnylinkEnabledBtn->setDescription(description);
|
||||
|
||||
updatePanel();
|
||||
});
|
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, this, &SunnylinkPanel::updatePanel);
|
||||
@@ -51,22 +89,81 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) {
|
||||
vlayout->addWidget(sunnylinkScroller);
|
||||
|
||||
main_layout->addWidget(sunnylinkScreen);
|
||||
|
||||
if (is_sunnylink_enabled) {
|
||||
startSunnylink();
|
||||
}
|
||||
}
|
||||
|
||||
void SunnylinkPanel::paramsRefresh(const QString ¶m_name, const QString ¶m_value) {
|
||||
// We do it on paramsRefresh because the toggleEvent happens before the value is updated
|
||||
if (param_name == "SunnylinkEnabled" && param_value == "1") {
|
||||
startSunnylink();
|
||||
} else if (param_name == "SunnylinkEnabled" && param_value == "0") {
|
||||
stopSunnylink();
|
||||
}
|
||||
|
||||
updatePanel();
|
||||
}
|
||||
|
||||
void SunnylinkPanel::startSunnylink() const {
|
||||
if (!sunnylink_client->role_service->isCurrentyPolling()) {
|
||||
sunnylink_client->role_service->startPolling();
|
||||
} else {
|
||||
sunnylink_client->role_service->load();
|
||||
}
|
||||
|
||||
if (!sunnylink_client->user_service->isCurrentyPolling()) {
|
||||
sunnylink_client->user_service->startPolling();
|
||||
} else {
|
||||
sunnylink_client->user_service->load();
|
||||
}
|
||||
}
|
||||
|
||||
void SunnylinkPanel::stopSunnylink() const {
|
||||
sunnylink_client->role_service->stopPolling();
|
||||
sunnylink_client->user_service->stopPolling();
|
||||
}
|
||||
|
||||
void SunnylinkPanel::showEvent(QShowEvent *event) {
|
||||
updatePanel(offroad);
|
||||
updatePanel();
|
||||
if (is_sunnylink_enabled) {
|
||||
startSunnylink();
|
||||
}
|
||||
}
|
||||
|
||||
void SunnylinkPanel::updatePanel(bool _offroad) {
|
||||
QString sunnylink_device_id = tr("Device ID") + " " + getSunnylinkDongleId().value_or(tr("N/A"));
|
||||
|
||||
sunnylinkEnabledBtn->setEnabled(_offroad);
|
||||
|
||||
if (sunnylinkEnabledBtn->isToggled()) {
|
||||
sunnylinkEnabledBtn->setValue(sunnylink_device_id);
|
||||
} else {
|
||||
sunnylinkEnabledBtn->setValue("");
|
||||
void SunnylinkPanel::updatePanel() {
|
||||
if (!isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
offroad = _offroad;
|
||||
const auto sunnylinkDongleId = getSunnylinkDongleId().value_or(tr("N/A"));
|
||||
sunnylinkEnabledBtn->setEnabled(!is_onroad);
|
||||
|
||||
is_sunnylink_enabled = Params().getBool("SunnylinkEnabled");
|
||||
bool is_sub = uiStateSP()->isSunnylinkSponsor() && is_sunnylink_enabled;
|
||||
auto max_current_sponsor_rule = uiStateSP()->sunnylinkSponsorRole();
|
||||
auto role_name = max_current_sponsor_rule.getSponsorTierString();
|
||||
std::optional role_color = max_current_sponsor_rule.getSponsorTierColor();
|
||||
bool is_paired = uiStateSP()->isSunnylinkPaired();
|
||||
auto paired_users = uiStateSP()->sunnylinkDeviceUsers();
|
||||
|
||||
sunnylinkEnabledBtn->setEnabled(!is_onroad);
|
||||
sunnylinkEnabledBtn->setValue(tr("Device ID") + " " + sunnylinkDongleId);
|
||||
|
||||
sponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled);
|
||||
sponsorBtn->setText(is_sub ? tr("THANKS ♥")/* + " ♥️"*/ : tr("SPONSOR"));
|
||||
sponsorBtn->setValue(is_sub ? tr(role_name.toStdString().c_str()) : tr("Not Sponsor"), role_color);
|
||||
|
||||
pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled);
|
||||
pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired"));
|
||||
|
||||
|
||||
if (!is_sunnylink_enabled) {
|
||||
sunnylinkEnabledBtn->setValue("");
|
||||
sponsorBtn->setValue("");
|
||||
pairSponsorBtn->setValue("");
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/sunnylink_client.h"
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink/sponsor_widget.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h"
|
||||
|
||||
class SunnylinkPanel : public QFrame {
|
||||
@@ -16,16 +19,29 @@ class SunnylinkPanel : public QFrame {
|
||||
public:
|
||||
explicit SunnylinkPanel(QWidget *parent = nullptr);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void paramsRefresh(const QString¶m_name, const QString¶m_value);
|
||||
|
||||
public slots:
|
||||
void updatePanel(bool _offroad);
|
||||
void updatePanel();
|
||||
|
||||
private:
|
||||
Params params;
|
||||
QStackedLayout *main_layout = nullptr;
|
||||
QWidget *sunnylinkScreen = nullptr;
|
||||
ScrollViewSP *sunnylinkScroller = nullptr;
|
||||
bool offroad;
|
||||
SunnylinkSponsorPopup *status_popup;
|
||||
SunnylinkSponsorPopup *pair_popup;
|
||||
ButtonControlSP* sponsorBtn;
|
||||
ButtonControlSP* pairSponsorBtn;
|
||||
SunnylinkClient* sunnylink_client;
|
||||
|
||||
ParamControl *sunnylinkEnabledBtn;
|
||||
bool is_onroad = false;
|
||||
bool is_backup = false;
|
||||
bool is_restore = false;
|
||||
bool is_sunnylink_enabled = false;
|
||||
ParamWatcher *param_watcher;
|
||||
QString sunnylinkBtnDescription;
|
||||
void stopSunnylink() const;
|
||||
void startSunnylink() const;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/request_repeater.h"
|
||||
|
||||
RequestRepeaterSP::RequestRepeaterSP(QObject *parent, const QString &requestURL, const QString &cacheKey,
|
||||
int period, bool whileOnroad, bool sunnylink) : HttpRequestSP(parent, true, 20000, sunnylink) {
|
||||
request_url = requestURL;
|
||||
while_onroad = whileOnroad;
|
||||
timer = new QTimer(this);
|
||||
timer->setTimerType(Qt::VeryCoarseTimer);
|
||||
connect(timer, &QTimer::timeout, [=]() { this->timerTick(); });
|
||||
timer->start(period * 1000);
|
||||
|
||||
if (!cacheKey.isEmpty()) {
|
||||
prevResp = QString::fromStdString(params.get(cacheKey.toStdString()));
|
||||
if (!prevResp.isEmpty()) {
|
||||
QTimer::singleShot(500, [=]() { emit requestDone(prevResp, true, QNetworkReply::NoError); });
|
||||
}
|
||||
connect(this, &HttpRequest::requestDone, [=](const QString &resp, bool success) {
|
||||
if (success && resp != prevResp) {
|
||||
params.put(cacheKey.toStdString(), resp.toStdString());
|
||||
prevResp = resp;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Don't wait for the timer to fire to send the first request
|
||||
ForceUpdate();
|
||||
}
|
||||
|
||||
void RequestRepeaterSP::timerTick() {
|
||||
if ((!uiState()->scene.started || while_onroad) && device()->isAwake() && !active()) {
|
||||
LOGD("Sending request for %s", qPrintable(request_url));
|
||||
sendRequest(request_url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
*
|
||||
* This file is part of sunnypilot and is licensed under the MIT License.
|
||||
* See the LICENSE.md file in the root directory for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "selfdrive/ui/qt/request_repeater.h"
|
||||
|
||||
#include "common/swaglog.h"
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/ui/sunnypilot/ui.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/api.h"
|
||||
|
||||
class RequestRepeaterSP : public HttpRequestSP {
|
||||
|
||||
private:
|
||||
Params params;
|
||||
QTimer *timer;
|
||||
QString prevResp;
|
||||
QString request_url;
|
||||
bool while_onroad;
|
||||
void timerTick();
|
||||
|
||||
public:
|
||||
RequestRepeaterSP(QObject *parent, const QString &requestURL, const QString &cacheKey = "", int period = 0, bool whileOnroad=false, bool sunnylink = false);
|
||||
void ForceUpdate() {
|
||||
LOGD("Forcing update for %s", qPrintable(request_url));
|
||||
timerTick();
|
||||
}
|
||||
};
|
||||
@@ -48,6 +48,16 @@ UIStateSP *uiStateSP() {
|
||||
return &ui_state;
|
||||
}
|
||||
|
||||
void UIStateSP::setSunnylinkRoles(const std::vector<RoleModel>& roles) {
|
||||
sunnylinkRoles = roles;
|
||||
emit sunnylinkRolesChanged(roles);
|
||||
}
|
||||
|
||||
void UIStateSP::setSunnylinkDeviceUsers(const std::vector<UserModel>& users) {
|
||||
sunnylinkUsers = users;
|
||||
emit sunnylinkDeviceUsersChanged(users);
|
||||
}
|
||||
|
||||
DeviceSP *deviceSP() {
|
||||
static DeviceSP _device;
|
||||
return &_device;
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/user_model.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h"
|
||||
#include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h"
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
class UIStateSP : public UIState {
|
||||
@@ -15,12 +20,54 @@ class UIStateSP : public UIState {
|
||||
public:
|
||||
UIStateSP(QObject *parent = 0);
|
||||
void updateStatus() override;
|
||||
void setSunnylinkRoles(const std::vector<RoleModel> &roles);
|
||||
void setSunnylinkDeviceUsers(const std::vector<UserModel> &users);
|
||||
|
||||
inline std::vector<RoleModel> sunnylinkDeviceRoles() const { return sunnylinkRoles; }
|
||||
inline bool isSunnylinkAdmin() const {
|
||||
return std::any_of(sunnylinkRoles.begin(), sunnylinkRoles.end(), [](const RoleModel &role) {
|
||||
return role.roleType == RoleType::Admin;
|
||||
});
|
||||
}
|
||||
inline bool isSunnylinkSponsor() const {
|
||||
return std::any_of(sunnylinkRoles.begin(), sunnylinkRoles.end(), [](const RoleModel &role) {
|
||||
return role.roleType == RoleType::Sponsor && role.as<SponsorRoleModel>().roleTier != SponsorTier::Free;
|
||||
});
|
||||
}
|
||||
inline SponsorRoleModel sunnylinkSponsorRole() const {
|
||||
std::optional<SponsorRoleModel> sponsorRoleWithHighestTier = std::nullopt;
|
||||
for (const auto &role : sunnylinkRoles) {
|
||||
if(role.roleType != RoleType::Sponsor)
|
||||
continue;
|
||||
|
||||
if (auto sponsorRole = role.as<SponsorRoleModel>(); !sponsorRoleWithHighestTier.has_value() || sponsorRoleWithHighestTier->roleTier < sponsorRole.roleTier) {
|
||||
sponsorRoleWithHighestTier = sponsorRole;
|
||||
}
|
||||
}
|
||||
return sponsorRoleWithHighestTier.value_or(SponsorRoleModel(RoleType::Sponsor, SponsorTier::Free));
|
||||
}
|
||||
inline SponsorTier sunnylinkSponsorTier() const {
|
||||
return sunnylinkSponsorRole().roleTier;
|
||||
}
|
||||
inline std::vector<UserModel> sunnylinkDeviceUsers() const { return sunnylinkUsers; }
|
||||
inline bool isSunnylinkPaired() const {
|
||||
return std::any_of(sunnylinkUsers.begin(), sunnylinkUsers.end(), [](const UserModel &user) {
|
||||
return user.user_id.toLower() != "unregisteredsponsor" && user.user_id.toLower() != "temporarysponsor";
|
||||
});
|
||||
}
|
||||
|
||||
signals:
|
||||
void sunnylinkRoleChanged(bool subscriber);
|
||||
void sunnylinkRolesChanged(std::vector<RoleModel> roles);
|
||||
void sunnylinkDeviceUsersChanged(std::vector<UserModel> users);
|
||||
void uiUpdate(const UIStateSP &s);
|
||||
|
||||
private slots:
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
std::vector<RoleModel> sunnylinkRoles = {};
|
||||
std::vector<UserModel> sunnylinkUsers = {};
|
||||
};
|
||||
|
||||
UIStateSP *uiStateSP();
|
||||
|
||||
@@ -207,6 +207,16 @@ def setup_settings_sunnylink(click, pm: PubMaster, scroll=None):
|
||||
click(278, 522)
|
||||
time.sleep(UI_DELAY)
|
||||
|
||||
def setup_settings_sunnylink_sponsor_button(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_sunnylink(click, pm)
|
||||
click(1967, 225)
|
||||
time.sleep(UI_DELAY)
|
||||
|
||||
def setup_settings_sunnylink_pair_button(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_sunnylink(click, pm)
|
||||
click(1967, 432)
|
||||
time.sleep(UI_DELAY)
|
||||
|
||||
def setup_settings_sunnypilot(click, pm: PubMaster, scroll=None):
|
||||
setup_settings_device(click, pm)
|
||||
click(278, 852)
|
||||
@@ -268,6 +278,8 @@ CASES = {
|
||||
|
||||
CASES.update({
|
||||
"settings_sunnylink": setup_settings_sunnylink,
|
||||
"settings_sunnylink_sponsor_button": setup_settings_sunnylink_sponsor_button,
|
||||
"settings_sunnylink_pair_button": setup_settings_sunnylink_pair_button,
|
||||
"settings_sunnypilot": setup_settings_sunnypilot,
|
||||
"settings_sunnypilot_mads": setup_settings_sunnypilot_mads,
|
||||
"settings_trips": setup_settings_trips,
|
||||
|
||||
@@ -981,7 +981,7 @@ This may take up to a minute.</source>
|
||||
</message>
|
||||
<message>
|
||||
<source>Firehose</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation type="unfinished">خرطوم الحريق</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -1450,6 +1450,89 @@ This may take up to a minute.</source>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">غير متاح</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished">إقران</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
@@ -1428,6 +1428,89 @@ This may take up to a minute.</source>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">Nicht verfügbar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
@@ -1428,6 +1428,93 @@ Esto puede tardar un minuto.</translation>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">N/A</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished">Estado de patrocinio</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished">PATROCINADOR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished">Conviértete en patrocinador de sunnypilot para obtener acceso anticipado a las funciones de sunnylink cuando estén disponibles.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished">Emparejar cuenta de GitHub</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished">EMPAREJAR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished">Empareja tu cuenta de GitHub para otorgar a tu dispositivo beneficios de patrocinador, incluido acceso a la API en sunnylink.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished">ID del dongle de sunnylink no encontrado. Esto puede deberse a una conexión débil o a un problema de registro en sunnylink. Por favor, reinicia y vuelve a intentarlo.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS</source>
|
||||
<translation type="obsolete">GRACIAS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished">No patrocinador</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished">Emparejado</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished">No emparejado</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished">GRACIAS ♥</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished">Escanea el código QR para iniciar sesión en tu cuenta de GitHub</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished">Sigue las indicaciones para completar el proceso de emparejamiento</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished">Vuelve a ingresar al panel de "sunnylink" para verificar el estado de patrocinio</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished">Si el estado de patrocinio no se actualizó, por favor contacta a un moderador en Discord en https://discord.gg/sunnypilot</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished">Escanea el código QR para visitar la página de GitHub Sponsors de sunnyhaibin</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished">Elige tu nivel de patrocinio y confirma tu apoyo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished">Únete a nuestra comunidad en Discord en https://discord.gg/sunnypilot y contacta a un moderador para confirmar tu estado de patrocinio</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished">Empareja tu cuenta de GitHub</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished">Acceso temprano: conviértete en patrocinador de sunnypilot</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
@@ -1428,6 +1428,89 @@ Cela peut prendre jusqu'à une minute.</translation>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">N/A</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished">ASSOCIER</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
@@ -954,7 +954,7 @@ This may take up to a minute.</source>
|
||||
</message>
|
||||
<message>
|
||||
<source>Firehose</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation type="unfinished">データ学習</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -1423,6 +1423,89 @@ This may take up to a minute.</source>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">該当なし</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished">OK</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
@@ -956,7 +956,7 @@ This may take up to a minute.</source>
|
||||
</message>
|
||||
<message>
|
||||
<source>Firehose</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation type="unfinished">파이어호스</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -1425,6 +1425,89 @@ This may take up to a minute.</source>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">N/A</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished">동기화</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
@@ -961,7 +961,7 @@ Isso pode levar até um minuto.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Firehose</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation type="unfinished">Firehose</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -1430,6 +1430,89 @@ Isso pode levar até um minuto.</translation>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">N/A</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished">PAREAR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
@@ -1423,6 +1423,89 @@ This may take up to a minute.</source>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">ไม่มี</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished">จับคู่</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
@@ -1421,6 +1421,89 @@ This may take up to a minute.</source>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">N/A</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
@@ -954,7 +954,7 @@ This may take up to a minute.</source>
|
||||
</message>
|
||||
<message>
|
||||
<source>Firehose</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation type="unfinished">训练上传</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -1423,6 +1423,89 @@ This may take up to a minute.</source>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">N/A</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished">配对</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
@@ -954,7 +954,7 @@ This may take up to a minute.</source>
|
||||
</message>
|
||||
<message>
|
||||
<source>Firehose</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation type="unfinished">訓練上傳</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -1423,6 +1423,89 @@ This may take up to a minute.</source>
|
||||
<source>N/A</source>
|
||||
<translation type="unfinished">無法使用</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Sponsor Status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>SPONSOR</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Become a sponsor of sunnypilot to get early access to sunnylink features when they become available.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair GitHub Account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>PAIR</source>
|
||||
<translation type="unfinished">配對</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Not Paired</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>THANKS ♥</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnylinkSponsorPopup</name>
|
||||
<message>
|
||||
<source>Scan the QR code to login to your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Follow the prompts to complete the pairing process</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Re-enter the "sunnylink" panel to verify sponsorship status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Scan the QR code to visit sunnyhaibin's GitHub Sponsors page</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose your sponsorship tier and confirm your support</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Pair your GitHub account</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Early Access: Become a sunnypilot Sponsor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SunnypilotPanel</name>
|
||||
|
||||
Reference in New Issue
Block a user