big refactoring of API system
This commit is contained in:
parent
27124380e4
commit
7a116bfdf2
@ -17,4 +17,4 @@ set(SOURCES
|
|||||||
|
|
||||||
target_sources(magpie PRIVATE ${SOURCES})
|
target_sources(magpie PRIVATE ${SOURCES})
|
||||||
|
|
||||||
add_subdirectory(models)
|
add_subdirectory(requests)
|
||||||
|
527
API/api.cpp
527
API/api.cpp
@ -1,5 +1,5 @@
|
|||||||
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include "api.h"
|
#include "api.h"
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
@ -7,466 +7,128 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QUrlQuery>
|
#include <QUrlQuery>
|
||||||
|
|
||||||
#include "codes.h"
|
#include "requests/test.h"
|
||||||
#include "finalaction.h"
|
#include "requests/register.h"
|
||||||
#include "helpers.h"
|
#include "requests/login.h"
|
||||||
|
#include "requests/poll.h"
|
||||||
|
#include "requests/listassets.h"
|
||||||
|
#include "requests/addasset.h"
|
||||||
|
|
||||||
constexpr const char* json = "application/json";
|
API::API (Models::Magpie& magpie, QObject* parent):
|
||||||
constexpr const char* urlEncoded = "application/x-www-form-urlencoded";
|
|
||||||
|
|
||||||
const std::map<QString, QMetaType::Type> testStructure({
|
|
||||||
{"type", QMetaType::QString},
|
|
||||||
{"version", QMetaType::QString},
|
|
||||||
});
|
|
||||||
|
|
||||||
const std::map<QString, QMetaType::Type> resultStructure({
|
|
||||||
{"result", QMetaType::LongLong},
|
|
||||||
});
|
|
||||||
|
|
||||||
const std::map<QString, QMetaType::Type> tokensStructure({
|
|
||||||
{"accessToken", QMetaType::QString},
|
|
||||||
{"renewToken", QMetaType::QString}
|
|
||||||
});
|
|
||||||
|
|
||||||
struct NetworkReplyDeleter {
|
|
||||||
void operator () (QNetworkReply* reply) {
|
|
||||||
reply->deleteLater();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
API::API(const QUrl& address, QObject* parent):
|
|
||||||
QObject(parent),
|
QObject(parent),
|
||||||
address(address),
|
idCounter(0),
|
||||||
|
magpie(magpie),
|
||||||
network(),
|
network(),
|
||||||
state(NoServer),
|
|
||||||
accessToken(),
|
|
||||||
renewToken(),
|
|
||||||
firstPoll(),
|
|
||||||
pollReply(),
|
pollReply(),
|
||||||
assets()
|
requests()
|
||||||
{
|
{}
|
||||||
firstPoll.setSingleShot(true);
|
|
||||||
firstPoll.setInterval(2000);
|
void API::cancelRequest (RequestId id) {
|
||||||
connect(&firstPoll, &QTimer::timeout, this, &API::onFirstPollSuccess);
|
requests.erase(id);
|
||||||
connect(&assets, &Models::Assets::requestAssets, this, &API::requestAssets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QUrl API::getAddress() const {
|
void API::onRequestDone (RequestId id) {
|
||||||
return address;
|
requests.erase(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
API::State API::getState() const {
|
API::RequestId API::test (const QString& path, const QJSValue& finished) {
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::setTokens(const QString access, const QString &renew) {
|
|
||||||
accessToken = access;
|
|
||||||
renewToken = renew;
|
|
||||||
setState(Authenticating);
|
|
||||||
startPolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::startPolling() {
|
|
||||||
qDebug() << "Starting polling...";
|
|
||||||
if (state != Authenticating)
|
|
||||||
throw std::runtime_error("Can not start polling in this state: " + std::to_string(state));
|
|
||||||
|
|
||||||
sendPoll();
|
|
||||||
firstPoll.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::setAddress(const QUrl& path) {
|
|
||||||
if (address == path)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (state == Authenticated) {
|
|
||||||
//do something
|
|
||||||
}
|
|
||||||
|
|
||||||
address = path;
|
|
||||||
emit addressChanged(address);
|
|
||||||
setState(address.isEmpty() ? NoServer : NotAuthenticated);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<QVariantMap> API::readResult(QNetworkReply* reply) {
|
|
||||||
QVariant contentType = reply->header(QNetworkRequest::ContentTypeHeader);
|
|
||||||
if (!
|
|
||||||
contentType.isValid() ||
|
|
||||||
contentType.userType() != QMetaType::QString ||
|
|
||||||
contentType.toString() != json
|
|
||||||
)
|
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
QByteArray data = reply->readAll();
|
|
||||||
QJsonDocument document = QJsonDocument::fromJson(data);
|
|
||||||
if (!document.isObject())
|
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
QJsonObject object = document.object();
|
|
||||||
return object.toVariantMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool API::validateResponse(const std::optional<QVariantMap>& data, const std::map<QString, QMetaType::Type>& structure) {
|
|
||||||
if (!data.has_value())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (const std::pair<const QString, QMetaType::Type>& pair : structure) {
|
|
||||||
QVariantMap::ConstIterator itr = data->find(pair.first);
|
|
||||||
if (itr == data->end())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (itr->userType() != pair.second)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::setState(State newState) {
|
|
||||||
if (newState == state)
|
|
||||||
return;
|
|
||||||
|
|
||||||
state = newState;
|
|
||||||
emit stateChanged(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
QUrl API::createUrl(const QString &path) const {
|
|
||||||
QString startingPath = address.path();
|
|
||||||
QUrl url = address;
|
|
||||||
url.setPath(startingPath + path);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::test(const QString& path, const QJSValue& finished) {
|
|
||||||
qDebug() << "Testing" << path;
|
qDebug() << "Testing" << path;
|
||||||
if (state == Offline)
|
if (magpie.getState() == Models::Magpie::Offline)
|
||||||
return callCallback(finished, "Need to be online to test");
|
callCallback(finished, "Need to be online to test"), 0;
|
||||||
|
|
||||||
QUrl address(path);
|
std::unique_ptr<Request::Test> test = std::make_unique<Request::Test>(path);
|
||||||
QNetworkRequest request(path + "/info");
|
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
|
|
||||||
|
|
||||||
QNetworkReply* reply = network.get(request);
|
|
||||||
connect(reply, &QNetworkReply::finished,
|
|
||||||
std::bind(&API::onTestFinished, this, reply, address, finished)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::onTestFinished(QNetworkReply* reply, const QUrl& addr, const QJSValue& finished) {
|
|
||||||
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
|
|
||||||
QNetworkReply::NetworkError error = reply->error();
|
|
||||||
if (error != QNetworkReply::NoError)
|
|
||||||
return callCallback(finished, reply->errorString());
|
|
||||||
|
|
||||||
std::optional<QVariantMap> data = readResult(reply);
|
|
||||||
if (!validateResponse(data, testStructure))
|
|
||||||
return callCallback(finished, "Malformed response");
|
|
||||||
|
|
||||||
QString type = data->value("type").toString();
|
|
||||||
if (type != "pica")
|
|
||||||
return callCallback(finished, "server of this type (" + type + ") is not supported");
|
|
||||||
|
|
||||||
QString version = data->value("version").toString();
|
|
||||||
if (version != "0.0.1")
|
|
||||||
return callCallback(finished, "server of this version (" + version + ") is not supported");
|
|
||||||
|
|
||||||
callCallback(finished, QString(), {QJSValue(true)});
|
|
||||||
address = ""; //to provoke singal change even if it's the same server
|
|
||||||
setAddress(addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::sendRegister(const QString& login, const QString& password, const QJSValue& finished) {
|
|
||||||
qDebug() << "Registering...";
|
|
||||||
if (state != NotAuthenticated)
|
|
||||||
return callCallback(finished, "Can not register in current state");
|
|
||||||
|
|
||||||
QUrlQuery params({
|
|
||||||
{"login", login},
|
|
||||||
{"password", password}
|
|
||||||
});
|
|
||||||
|
|
||||||
QNetworkRequest request(createUrl("/register"));
|
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded);
|
|
||||||
|
|
||||||
QNetworkReply* reply = network.post(request, params.toString(QUrl::FullyEncoded).toUtf8());
|
|
||||||
connect(reply, &QNetworkReply::finished,
|
|
||||||
std::bind(&API::onRegisterFinished, this, reply, finished)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::onRegisterFinished(QNetworkReply* reply, const QJSValue& finished) const {
|
|
||||||
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
|
|
||||||
QNetworkReply::NetworkError error = reply->error();
|
|
||||||
std::optional<QVariantMap> data = readResult(reply);
|
|
||||||
std::optional<Codes::Register> code;
|
|
||||||
QString detail;
|
|
||||||
bool success = false;
|
|
||||||
if (validateResponse(data, resultStructure)) {
|
|
||||||
code = Codes::convertRegister(data->value("result").toInt());
|
|
||||||
success = code.value() == Codes::Register::success;
|
|
||||||
if (!success)
|
|
||||||
detail = Codes::description(code.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error != QNetworkReply::NoError)
|
|
||||||
return callCallback(finished, reply->errorString() + (success ? "" : ": " + detail));
|
|
||||||
|
|
||||||
if (!code)
|
|
||||||
return callCallback(finished, "Malformed result");
|
|
||||||
|
|
||||||
callCallback(finished, detail, {QJSValue(success)});
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::sendLogin(const QString& login, const QString& password, const QJSValue& finished) {
|
|
||||||
qDebug() << "Logging in...";
|
|
||||||
if (state != NotAuthenticated)
|
|
||||||
return callCallback(finished, "Can not register in current state");
|
|
||||||
|
|
||||||
QUrlQuery params({
|
|
||||||
{"login", login},
|
|
||||||
{"password", password}
|
|
||||||
});
|
|
||||||
|
|
||||||
QNetworkRequest request(createUrl("/login"));
|
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded);
|
|
||||||
setState(Authenticating);
|
|
||||||
QNetworkReply* reply = network.post(request, params.toString(QUrl::FullyEncoded).toUtf8());
|
|
||||||
connect(reply, &QNetworkReply::finished,
|
|
||||||
std::bind(&API::onLoginFinished, this, reply, finished)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::onLoginFinished(QNetworkReply* reply, const QJSValue& finished) {
|
|
||||||
State state = NotAuthenticated;
|
|
||||||
FinalAction action([this, &state]() { //this should be executed on leaving the function in any way
|
|
||||||
setState(state); //setting the state as a result of this object destruction
|
|
||||||
}); //this way any error will result in NotAuthenticated, and on success it should be Authenticated
|
|
||||||
|
|
||||||
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
|
|
||||||
QNetworkReply::NetworkError error = reply->error();
|
|
||||||
std::optional<QVariantMap> data = readResult(reply);
|
|
||||||
std::optional<Codes::Login> code;
|
|
||||||
QString detail;
|
|
||||||
bool success = false;
|
|
||||||
if (validateResponse(data, resultStructure)) {
|
|
||||||
code = Codes::convertLogin(data->value("result").toInt());
|
|
||||||
success = code.value() == Codes::Login::success;
|
|
||||||
if (!success)
|
|
||||||
detail = Codes::description(code.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error != QNetworkReply::NoError)
|
|
||||||
return callCallback(finished, reply->errorString() + (success ? "" : ": " + detail));
|
|
||||||
|
|
||||||
if (!code)
|
|
||||||
return callCallback(finished, "Malformed result");
|
|
||||||
|
|
||||||
if (!validateResponse(data, tokensStructure))
|
|
||||||
return callCallback(finished, "Malformed result: missing tokens");
|
|
||||||
|
|
||||||
|
|
||||||
callCallback(finished, detail, {QJSValue(success)});
|
|
||||||
|
|
||||||
state = Authenticating;
|
|
||||||
accessToken = data->value("accessToken").toString();
|
|
||||||
renewToken = data->value("renewToken").toString();
|
|
||||||
emit storeTokens(accessToken, renewToken);
|
|
||||||
startPolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::addAsset(const QString& title, const QString& icon, const QJSValue &finished) {
|
|
||||||
qDebug() << "Adding asset...";
|
|
||||||
if (state != Authenticated)
|
|
||||||
return callCallback(finished, "Can not add assets in current state");
|
|
||||||
|
|
||||||
QUrlQuery params({
|
|
||||||
{"title", title},
|
|
||||||
{"icon", icon},
|
|
||||||
{"currency", "1"}
|
|
||||||
});
|
|
||||||
|
|
||||||
QNetworkRequest request(createUrl("/addAsset"));
|
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded);
|
|
||||||
request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8());
|
|
||||||
QNetworkReply* reply = network.post(request, params.toString(QUrl::FullyEncoded).toUtf8());
|
|
||||||
connect(reply, &QNetworkReply::finished,
|
|
||||||
this, std::bind(&API::onAssetAdded, this, reply, finished),
|
|
||||||
Qt::QueuedConnection
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::onAssetAdded(QNetworkReply *reply, const QJSValue &finished) {
|
|
||||||
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
|
|
||||||
QNetworkReply::NetworkError error = reply->error();
|
|
||||||
|
|
||||||
qDebug() << error;
|
|
||||||
if (error != QNetworkReply::NoError)
|
|
||||||
return callCallback(finished, reply->errorString());
|
|
||||||
|
|
||||||
callCallback(finished, QString(), {QJSValue(true)});
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::sendPoll(bool clear) {
|
|
||||||
if (accessToken.isEmpty())
|
|
||||||
throw std::runtime_error("Can not start polling: access token is empty");
|
|
||||||
|
|
||||||
QUrl url = createUrl("/poll");
|
|
||||||
if (clear)
|
|
||||||
url.setQuery(QUrlQuery({{"clearCache", "all"}}));
|
|
||||||
|
|
||||||
QNetworkRequest request(url);
|
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
|
|
||||||
request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8());
|
|
||||||
request.setTransferTimeout(30000);
|
|
||||||
|
|
||||||
pollReply = std::unique_ptr<QNetworkReply>(network.get(request));
|
|
||||||
connect(
|
connect(
|
||||||
pollReply.get(), &QNetworkReply::finished,
|
test.get(),
|
||||||
this, &API::onPollFinished, Qt::QueuedConnection
|
&Request::Test::success,
|
||||||
|
this,
|
||||||
|
[this, &path, &finished] (const QVariantMap& data) {
|
||||||
|
callCallback(finished, QString(), {QJSValue(true)});
|
||||||
|
magpie.forceAddress(path);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
connect(test.get(), &Request::Test::error, std::bind(&API::callCallback, this, finished, std::placeholders::_1, QJSValueList(false)));
|
||||||
|
return registerAndSend(std::move(test));
|
||||||
}
|
}
|
||||||
|
|
||||||
void API::onPollFinished() {
|
API::RequestId API::sendRegister (const QString& login, const QString& password, const QJSValue& finished) {
|
||||||
firstPoll.stop();
|
qDebug() << "Registering...";
|
||||||
QNetworkReply::NetworkError error = pollReply->error();
|
if (magpie.getState() != Models::Magpie::NotAuthenticated)
|
||||||
std::optional<QVariantMap> data = readResult(pollReply.get());
|
return callCallback(finished, "Can not register in current state"), 0;
|
||||||
Codes::Poll code = Codes::Poll::unknownError;
|
|
||||||
if (validateResponse(data, resultStructure))
|
|
||||||
code = Codes::convertPoll(data->value("result").toInt());
|
|
||||||
else
|
|
||||||
qDebug() << "";
|
|
||||||
|
|
||||||
QString detail = Codes::description(code);
|
std::unique_ptr<Request::Register> reg = std::make_unique<Request::Register>(login, password, magpie.getAddress());
|
||||||
|
connect(reg.get(), &Request::Register::success, std::bind(&API::callCallback, this, finished, QString(), QJSValueList{QJSValue(true)}));
|
||||||
|
connect(reg.get(), &Request::Register::error, std::bind(&API::callCallback, this, finished, std::placeholders::_1, QJSValueList{QJSValue(false)}));
|
||||||
|
return registerAndSend(std::move(reg));
|
||||||
|
}
|
||||||
|
|
||||||
if (error != QNetworkReply::NoError)
|
API::RequestId API::sendLogin (const QString& login, const QString& password, const QJSValue& finished) {
|
||||||
qDebug() << pollReply->errorString() + ": " + detail;
|
qDebug() << "Logging in...";
|
||||||
else
|
if (magpie.getState() != Models::Magpie::NotAuthenticated)
|
||||||
qDebug() << "Poll finished: " + detail;
|
return callCallback(finished, "Can not register in current state"), 0;
|
||||||
|
|
||||||
bool clear = false;
|
std::unique_ptr<Request::Login> log = std::make_unique<Request::Login>(login, password, magpie.getAddress());
|
||||||
switch (code) {
|
connect(
|
||||||
case Codes::Poll::success: {
|
log.get(),
|
||||||
QVariantMap::ConstIterator itr = data->constFind("data");
|
&Request::Login::success,
|
||||||
if (itr == data->constEnd())
|
this,
|
||||||
qDebug("received a poll that claimed to have some updates, but the \"data\" field is abscent");
|
[this, &finished] (const QVariantMap& data) {
|
||||||
|
callCallback(finished, QString(), {QJSValue(true)});
|
||||||
const QVariant& vdata = itr.value();
|
magpie.setTokens(data.value("accessToken").toString(), data.value("renewToken").toString());
|
||||||
if (vdata.canConvert<QVariantMap>())
|
|
||||||
clear = handleChanges(qast<QVariantMap>(itr.value()));
|
|
||||||
else
|
|
||||||
qDebug("received a poll that claimed to have some updates, but the \"data\" field is not an object");
|
|
||||||
}
|
}
|
||||||
//todo handle the result
|
);
|
||||||
case Codes::Poll::timeout:
|
connect(
|
||||||
setState(Authenticated);
|
log.get(),
|
||||||
return sendPoll(clear);
|
&Request::Login::error,
|
||||||
case Codes::Poll::tokenProblem:
|
this,
|
||||||
case Codes::Poll::replace:
|
[this, &finished] (const QString& error) {
|
||||||
case Codes::Poll::unknownError: //todo this one doesn't actually mean that we can't work for now, the network may be temporarily down or something
|
callCallback(finished, error, {QJSValue(false)});
|
||||||
pollReply.reset();
|
magpie.setState(Models::Magpie::NotAuthenticated);
|
||||||
return setState(NotAuthenticated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool API::handleChanges(const QVariantMap& changes) {
|
|
||||||
QVariantMap::ConstIterator itr = changes.constFind("system");
|
|
||||||
if (itr != changes.constEnd() && itr.value().canConvert<QVariantMap>()) {
|
|
||||||
const QVariantMap& sys = qast<QVariantMap>(itr.value());
|
|
||||||
QVariantMap::ConstIterator invItr = sys.constFind("invalidate");
|
|
||||||
if (invItr != sys.constEnd()) {
|
|
||||||
const QVariant& vinv = invItr.value();
|
|
||||||
if (vinv.canConvert<bool>() && vinv.toBool())
|
|
||||||
resetAllModels();
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
itr = changes.constFind("assets");
|
magpie.setState(Models::Magpie::Authenticating);
|
||||||
if (itr != changes.constEnd() && itr.value().canConvert<QVariantMap>()) {
|
return registerAndSend(std::move(log));
|
||||||
const QVariantMap& assets = qast<QVariantMap>(itr.value());
|
|
||||||
QVariantMap::ConstIterator aItr = assets.constFind("invalidate");
|
|
||||||
if (aItr != assets.constEnd()) {
|
|
||||||
const QVariant& vinv = aItr.value();
|
|
||||||
if (vinv.canConvert<bool>() && vinv.toBool())
|
|
||||||
API::assets.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
aItr = assets.constFind("added");
|
|
||||||
if (aItr != assets.constEnd() && aItr.value().canConvert<QVariantList>()) {
|
|
||||||
std::deque<Models::Asset> added;
|
|
||||||
if (!Models::Assets::deserialize(qast<QVariantList>(aItr.value()), added))
|
|
||||||
qDebug() << "Error deserializng added assets";
|
|
||||||
else
|
|
||||||
API::assets.addAssets(added);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void API::resetAllModels() {
|
API::RequestId API::addAsset (const QString& title, const QString& icon, const QJSValue& finished) {
|
||||||
assets.clear();
|
qDebug() << "Adding asset...";
|
||||||
|
if (magpie.getState() != Models::Magpie::Authenticated)
|
||||||
|
return callCallback(finished, "Can not add assets in current state"), 0;
|
||||||
|
|
||||||
|
auto add = std::make_unique<Request::AddAsset>(title, icon, QColor::fromString("black"), 1, magpie.getAddress());
|
||||||
|
add->setAuthorizationToken(magpie.getAccessToken());
|
||||||
|
connect(add.get(), &Request::AddAsset::success, std::bind(&API::callCallback, this, finished, QString(), QJSValueList{QJSValue(true)}));
|
||||||
|
connect(add.get(), &Request::AddAsset::error, std::bind(&API::callCallback, this, finished, std::placeholders::_1, QJSValueList{QJSValue(false)}));
|
||||||
|
return registerAndSend(std::move(add));
|
||||||
}
|
}
|
||||||
|
|
||||||
void API::onFirstPollSuccess() {
|
API::RequestId API::requestAssets (const SuccessListHandler& success, const ErrorHandler& error) {
|
||||||
setState(Authenticated);
|
|
||||||
}
|
|
||||||
|
|
||||||
void API::requestAssets() {
|
|
||||||
if (state != Authenticated) {
|
|
||||||
qDebug() << "An attempt to request assets on unauthenticated state";
|
|
||||||
assets.receivedAssets({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
qDebug() << "Requesting assets...";
|
qDebug() << "Requesting assets...";
|
||||||
|
|
||||||
QUrl url = createUrl("/listAssets");
|
auto list = std::make_unique<Request::ListAssets>(magpie.getAddress());
|
||||||
QNetworkRequest request(url);
|
list->setAuthorizationToken(magpie.getAccessToken());
|
||||||
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
|
connect(list.get(), &Request::ListAssets::success, success);
|
||||||
request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8());
|
connect(list.get(), &Request::ListAssets::error, error);
|
||||||
|
return registerAndSend(std::move(list));
|
||||||
QNetworkReply* reply = network.get(request);
|
|
||||||
connect(
|
|
||||||
reply, &QNetworkReply::finished,
|
|
||||||
this, std::bind(&API::responseAssets, this, reply)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void API::responseAssets(QNetworkReply *reply) {
|
API::RequestId API::poll (const SuccessMapHandler& success, const ErrorHandler& error, bool clear) {
|
||||||
std::deque<Models::Asset> result;
|
auto poll = std::make_unique<Request::Poll>(magpie.getAddress(), clear);
|
||||||
FinalAction action([this, &result]() {
|
poll->setAuthorizationToken(magpie.getAccessToken());
|
||||||
assets.receivedAssets(result);
|
connect(poll.get(), &Request::Poll::success, success);
|
||||||
});
|
connect(poll.get(), &Request::Poll::error, error);
|
||||||
|
return registerAndSend(std::move(poll));
|
||||||
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
|
|
||||||
QNetworkReply::NetworkError error = reply->error();
|
|
||||||
|
|
||||||
if (error != QNetworkReply::NoError) {
|
|
||||||
qDebug() << "Error receiving assets:" << reply->errorString();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<QVariantMap> data = readResult(reply);
|
|
||||||
if (!data) {
|
|
||||||
qDebug() << "Error receiving assets: bad data";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariantMap::ConstIterator itr = data->find("assets");
|
|
||||||
if (itr == data->constEnd() || !itr->canConvert<QVariantList>()) {
|
|
||||||
qDebug() << "Error receiving assets: assets are missing or not in an array";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Models::Assets::deserialize(qast<QVariantList>(itr.value()), result)) {
|
|
||||||
qDebug() << "Error deserializng assets";
|
|
||||||
result.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
qDebug() << "Assets successfully received";
|
|
||||||
//final action goes automatically
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void API::callCallback(const QJSValue& callback, const QString& error, const QJSValueList& arguments) const {
|
API::RequestId API::registerAndSend (std::unique_ptr<Request::Request> request) {
|
||||||
|
Request::Request* req = request.get();
|
||||||
|
requests.emplace(++idCounter, std::move(request));
|
||||||
|
connect(req, &Request::Request::done, std::bind(&API::onRequestDone, this, idCounter));
|
||||||
|
req->send(network);
|
||||||
|
return idCounter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void API::callCallback (const QJSValue& callback, const QString& error, const QJSValueList& arguments) const {
|
||||||
if (callback.isCallable()) {
|
if (callback.isCallable()) {
|
||||||
if (error.isEmpty())
|
if (error.isEmpty())
|
||||||
callback.call(QJSValueList({QJSValue(QJSValue::NullValue)}) + arguments);
|
callback.call(QJSValueList({QJSValue(QJSValue::NullValue)}) + arguments);
|
||||||
@ -474,4 +136,3 @@ void API::callCallback(const QJSValue& callback, const QString& error, const QJS
|
|||||||
callback.call({QJSValue(error)});
|
callback.call({QJSValue(error)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
78
API/api.h
78
API/api.h
@ -6,6 +6,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
@ -13,80 +14,45 @@
|
|||||||
#include <QJSValue>
|
#include <QJSValue>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
#include "models/assets.h"
|
#include "models/magpie.h"
|
||||||
|
#include "requests/request.h"
|
||||||
|
|
||||||
class Root;
|
|
||||||
class API : public QObject {
|
class API : public QObject {
|
||||||
friend class Root;
|
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
|
||||||
enum State {
|
|
||||||
Offline,
|
|
||||||
NoServer,
|
|
||||||
NotAuthenticated,
|
|
||||||
Authenticating,
|
|
||||||
Authenticated
|
|
||||||
};
|
|
||||||
Q_ENUM(State)
|
|
||||||
|
|
||||||
private:
|
|
||||||
Q_PROPERTY(QUrl address READ getAddress NOTIFY addressChanged)
|
|
||||||
Q_PROPERTY(State state READ getState NOTIFY stateChanged)
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit API(const QUrl& path = QString(), QObject* parent = nullptr);
|
using SuccessMapHandler = std::function<void(const QVariantMap&)>;
|
||||||
|
using SuccessListHandler = std::function<void(const QVariantList&)>;
|
||||||
|
using ErrorHandler = std::function<void(const QString&, const std::optional<QVariantMap>&)>;
|
||||||
|
using RequestId = unsigned int;
|
||||||
|
|
||||||
QUrl getAddress() const;
|
explicit API(Models::Magpie& magpie, QObject* parent = nullptr);
|
||||||
State getState() const;
|
|
||||||
void setTokens(const QString access, const QString& renew);
|
|
||||||
void startPolling();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void addressChanged(const QUrl& path);
|
|
||||||
void stateChanged(State state);
|
|
||||||
|
|
||||||
void storeTokens(const QString& access, const QString& renew);
|
RequestId requestAssets(const SuccessListHandler& success, const ErrorHandler& error);
|
||||||
|
RequestId poll(const SuccessMapHandler& success, const ErrorHandler& error, bool clear = false);
|
||||||
|
|
||||||
|
static const RequestId none = 0;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void test(const QString& path, const QJSValue& finished = QJSValue());
|
void cancelRequest(RequestId id);
|
||||||
void sendRegister(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
|
RequestId test(const QString& path, const QJSValue& finished = QJSValue());
|
||||||
void sendLogin(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
|
RequestId sendRegister(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
|
||||||
void addAsset(const QString& title, const QString& icon, const QJSValue& finished = QJSValue());
|
RequestId sendLogin(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
|
||||||
|
RequestId addAsset(const QString& title, const QString& icon, const QJSValue& finished = QJSValue());
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onTestFinished(QNetworkReply* reply, const QUrl& addr, const QJSValue& finished);
|
void onRequestDone(RequestId id);
|
||||||
void onRegisterFinished(QNetworkReply* reply, const QJSValue& finished) const;
|
|
||||||
void onLoginFinished(QNetworkReply* reply, const QJSValue& finished);
|
|
||||||
void onAssetAdded(QNetworkReply* reply, const QJSValue& finished);
|
|
||||||
void onPollFinished();
|
|
||||||
|
|
||||||
void onFirstPollSuccess();
|
|
||||||
|
|
||||||
void requestAssets();
|
|
||||||
void responseAssets(QNetworkReply* reply);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void callCallback(const QJSValue& callback, const QString& error = QString(), const QJSValueList& arguments = QJSValueList()) const;
|
void callCallback(const QJSValue& callback, const QString& error = QString(), const QJSValueList& arguments = QJSValueList()) const;
|
||||||
void setAddress(const QUrl& path);
|
RequestId registerAndSend(std::unique_ptr<Request::Request> request);
|
||||||
static std::optional<QVariantMap> readResult(QNetworkReply* reply);
|
|
||||||
static bool validateResponse(const std::optional<QVariantMap>& data, const std::map<QString, QMetaType::Type>& structure);
|
|
||||||
void setState(State newState);
|
|
||||||
QUrl createUrl(const QString& path) const;
|
|
||||||
void sendPoll(bool clear = false);
|
|
||||||
bool handleChanges(const QVariantMap& changes);
|
|
||||||
void resetAllModels();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QUrl address;
|
RequestId idCounter;
|
||||||
|
Models::Magpie& magpie;
|
||||||
QNetworkAccessManager network;
|
QNetworkAccessManager network;
|
||||||
State state;
|
std::map<RequestId, std::unique_ptr<Request::Request>> requests;
|
||||||
QString accessToken;
|
|
||||||
QString renewToken;
|
|
||||||
QTimer firstPoll;
|
|
||||||
std::unique_ptr<QNetworkReply> pollReply;
|
std::unique_ptr<QNetworkReply> pollReply;
|
||||||
|
|
||||||
public:
|
|
||||||
Models::Assets assets;
|
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#define UNUSED(X) (void)(X)
|
||||||
|
|
||||||
template <class T>
|
template <class T>
|
||||||
const T& qast(const QVariant& variant) {
|
const T& qast(const QVariant& variant) {
|
||||||
if (variant.userType() == qMetaTypeId<T>())
|
if (variant.userType() == qMetaTypeId<T>())
|
||||||
|
26
API/requests/CMakeLists.txt
Normal file
26
API/requests/CMakeLists.txt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
set(HEADERS
|
||||||
|
request.h
|
||||||
|
test.h
|
||||||
|
post.h
|
||||||
|
register.h
|
||||||
|
login.h
|
||||||
|
poll.h
|
||||||
|
listassets.h
|
||||||
|
addasset.h
|
||||||
|
)
|
||||||
|
|
||||||
|
set(SOURCES
|
||||||
|
request.cpp
|
||||||
|
test.cpp
|
||||||
|
post.cpp
|
||||||
|
register.cpp
|
||||||
|
login.cpp
|
||||||
|
poll.cpp
|
||||||
|
listassets.cpp
|
||||||
|
addasset.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_sources(magpie PRIVATE ${SOURCES})
|
10
API/requests/addasset.cpp
Normal file
10
API/requests/addasset.cpp
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "addasset.h"
|
||||||
|
|
||||||
|
Request::AddAsset::AddAsset (const QString& title, const QString& icon, const QColor& color, unsigned int currency, const QUrl& baseUrl):
|
||||||
|
Post(createUrl(baseUrl, "/addAsset"), {{"title", title}, {"icon", icon}, {"currency", std::to_string(currency).c_str()}, {"color", "0"}})
|
||||||
|
{
|
||||||
|
emptyResult = true;
|
||||||
|
}
|
19
API/requests/addasset.h
Normal file
19
API/requests/addasset.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
|
||||||
|
#include "post.h"
|
||||||
|
|
||||||
|
namespace Request {
|
||||||
|
|
||||||
|
class AddAsset : public Post {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
AddAsset (const QString& title, const QString& icon, const QColor& color, unsigned int currency, const QUrl& baseUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
18
API/requests/listassets.cpp
Normal file
18
API/requests/listassets.cpp
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "listassets.h"
|
||||||
|
|
||||||
|
#include "API/helpers.h"
|
||||||
|
|
||||||
|
Request::ListAssets::ListAssets (const QUrl& baseUrl):
|
||||||
|
Request(createUrl(baseUrl, "/listAssets")) {}
|
||||||
|
|
||||||
|
void Request::ListAssets::onSuccess (const QVariantMap& data) {
|
||||||
|
QVariantMap::ConstIterator itr = data.find("assets");
|
||||||
|
if (itr == data.constEnd() || !itr->canConvert<QVariantList>())
|
||||||
|
return Request::onError("Error receiving assets: assets are missing or not in an array", std::nullopt);
|
||||||
|
|
||||||
|
emit success(qast<QVariantList>(itr.value()));
|
||||||
|
emit done();
|
||||||
|
}
|
23
API/requests/listassets.h
Normal file
23
API/requests/listassets.h
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "request.h"
|
||||||
|
|
||||||
|
namespace Request {
|
||||||
|
|
||||||
|
class ListAssets : public Request {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
ListAssets (const QUrl& baseUrl);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void success(const QVariantList& assets);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onSuccess (const QVariantMap& data) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
34
API/requests/login.cpp
Normal file
34
API/requests/login.cpp
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "login.h"
|
||||||
|
|
||||||
|
#include "API/codes.h"
|
||||||
|
|
||||||
|
const std::map<QString, QMetaType::Type> Request::Login::tokensStructure({
|
||||||
|
{"accessToken", QMetaType::QString}, {"renewToken", QMetaType::QString}
|
||||||
|
});
|
||||||
|
|
||||||
|
Request::Login::Login (const QString& login, const QString& password, const QUrl& baseUrl):
|
||||||
|
Post(createUrl(baseUrl, "/login"), {{"login", login}, {"password", password}}) {}
|
||||||
|
|
||||||
|
void Request::Login::onSuccess (const QVariantMap& data) {
|
||||||
|
if (!validateResponse(data, resultStructure))
|
||||||
|
return Request::onError("Malformed response", std::nullopt);
|
||||||
|
|
||||||
|
if (!validateResponse(data, tokensStructure))
|
||||||
|
return Request::onError("Malformed result: missing tokens", std::nullopt);
|
||||||
|
|
||||||
|
Codes::Login code = Codes::convertLogin(data.value("result").toInt());
|
||||||
|
if (code != Codes::Login::success)
|
||||||
|
return Request::onError("Failed to login: " + Codes::description(code), data);
|
||||||
|
|
||||||
|
Request::onSuccess(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Login::onError (const QString& err, const std::optional<QVariantMap>& data) {
|
||||||
|
if (validateResponse(data, resultStructure))
|
||||||
|
Request::onError(err + ": " + Codes::description(Codes::convertLogin(data->value("result").toInt())), data);
|
||||||
|
else
|
||||||
|
Request::onError(err, data);
|
||||||
|
}
|
23
API/requests/login.h
Normal file
23
API/requests/login.h
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "post.h"
|
||||||
|
|
||||||
|
namespace Request {
|
||||||
|
|
||||||
|
class Login : public Post {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
Login (const QString& login, const QString& password, const QUrl& baseUrl);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onSuccess (const QVariantMap& data) override;
|
||||||
|
void onError (const QString& error, const std::optional<QVariantMap>& data) override;
|
||||||
|
|
||||||
|
static const std::map<QString, QMetaType::Type> tokensStructure;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
46
API/requests/poll.cpp
Normal file
46
API/requests/poll.cpp
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "poll.h"
|
||||||
|
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
#include "API/codes.h"
|
||||||
|
|
||||||
|
const std::map<QString, QMetaType::Type> Request::Poll::updatesStructure({
|
||||||
|
{"data", QMetaType::QVariantMap},
|
||||||
|
});
|
||||||
|
|
||||||
|
Request::Poll::Poll (const QUrl& baseUrl, bool clear):
|
||||||
|
Request(createUrl(baseUrl, "/poll", clear))
|
||||||
|
{
|
||||||
|
request.setTransferTimeout(30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Poll::onSuccess (const QVariantMap& data) {
|
||||||
|
if (!validateResponse(data, resultStructure))
|
||||||
|
return Request::onError("Malformed response", std::nullopt);
|
||||||
|
|
||||||
|
Codes::Poll code = Codes::convertPoll(data.value("result").toInt());
|
||||||
|
if (code == Codes::Poll::success)
|
||||||
|
if (!validateResponse(data, updatesStructure))
|
||||||
|
return onError("Malformed response: received a poll that claimed to have some updates, but the \"data\" field is abscent", data);
|
||||||
|
|
||||||
|
Request::onSuccess(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Poll::onError (const QString& err, const std::optional<QVariantMap>& data) {
|
||||||
|
if (validateResponse(data, resultStructure))
|
||||||
|
Request::onError(err + ": " + Codes::description(Codes::convertPoll(data->value("result").toInt())), data);
|
||||||
|
else
|
||||||
|
Request::onError(err, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl Request::Poll::createUrl (QUrl base, const QString& path, bool clear) {
|
||||||
|
QString startingPath = base.path();
|
||||||
|
base.setPath(startingPath + path);
|
||||||
|
if (clear)
|
||||||
|
base.setQuery(QUrlQuery({{"clearCache", "all"}}));
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
25
API/requests/poll.h
Normal file
25
API/requests/poll.h
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "request.h"
|
||||||
|
|
||||||
|
namespace Request {
|
||||||
|
|
||||||
|
class Poll : public Request {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
Poll (const QUrl& baseUrl, bool clear = false);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onSuccess (const QVariantMap& data) override;
|
||||||
|
void onError (const QString& error, const std::optional<QVariantMap>& data) override;
|
||||||
|
|
||||||
|
static QUrl createUrl(QUrl base, const QString& path, bool clear);
|
||||||
|
|
||||||
|
static const std::map<QString, QMetaType::Type> updatesStructure;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
17
API/requests/post.cpp
Normal file
17
API/requests/post.cpp
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "post.h"
|
||||||
|
|
||||||
|
Request::Post::Post (const QUrl& url, const QUrlQuery& form):
|
||||||
|
Request(url),
|
||||||
|
form(form)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void Request::Post::aquireRequest (QNetworkAccessManager& manager) {
|
||||||
|
reply = std::unique_ptr<QNetworkReply, NetworkReplyDeleter>(manager.post(request, form.toString(QUrl::FullyEncoded).toUtf8()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Post::initializeRequest () {
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded);
|
||||||
|
}
|
25
API/requests/post.h
Normal file
25
API/requests/post.h
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "request.h"
|
||||||
|
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
namespace Request {
|
||||||
|
|
||||||
|
class Post : public Request {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
Post(const QUrl& url, const QUrlQuery& form);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void aquireRequest (QNetworkAccessManager &manager) override;
|
||||||
|
void initializeRequest () override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QUrlQuery form;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
29
API/requests/register.cpp
Normal file
29
API/requests/register.cpp
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "register.h"
|
||||||
|
|
||||||
|
#include <QUrlQuery>
|
||||||
|
|
||||||
|
#include "API/codes.h"
|
||||||
|
|
||||||
|
Request::Register::Register (const QString& login, const QString& password, const QUrl& baseUrl):
|
||||||
|
Post(createUrl(baseUrl, "/register"), {{"login", login}, {"password", password}}) {}
|
||||||
|
|
||||||
|
void Request::Register::onSuccess (const QVariantMap& data) {
|
||||||
|
if (!validateResponse(data, resultStructure))
|
||||||
|
return Request::onError("Malformed response", std::nullopt);
|
||||||
|
|
||||||
|
Codes::Register code = Codes::convertRegister(data.value("result").toInt());
|
||||||
|
if (code != Codes::Register::success)
|
||||||
|
return Request::onError("Failed to register: " + Codes::description(code), data);
|
||||||
|
|
||||||
|
Request::onSuccess(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Register::onError (const QString& err, const std::optional<QVariantMap>& data) {
|
||||||
|
if (validateResponse(data, resultStructure))
|
||||||
|
Request::onError(err + ": " + Codes::description(Codes::convertRegister(data->value("result").toInt())), data);
|
||||||
|
else
|
||||||
|
Request::onError(err, data);
|
||||||
|
}
|
21
API/requests/register.h
Normal file
21
API/requests/register.h
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "post.h"
|
||||||
|
|
||||||
|
namespace Request {
|
||||||
|
|
||||||
|
class Register : public Post {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
Register (const QString& login, const QString& password, const QUrl& baseUrl);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onSuccess (const QVariantMap& data) override;
|
||||||
|
void onError (const QString& error, const std::optional<QVariantMap>& data) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
134
API/requests/request.cpp
Normal file
134
API/requests/request.cpp
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "request.h"
|
||||||
|
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
const std::map<QString, QMetaType::Type> Request::Request::resultStructure({
|
||||||
|
{"result", QMetaType::LongLong},
|
||||||
|
});
|
||||||
|
|
||||||
|
Request::Request::Request (const QUrl& url):
|
||||||
|
state(State::initial),
|
||||||
|
request(url),
|
||||||
|
reply(),
|
||||||
|
emptyResult(false)
|
||||||
|
{}
|
||||||
|
|
||||||
|
Request::Request::~Request () {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Request::send (QNetworkAccessManager& manager) {
|
||||||
|
if (state != State::initial)
|
||||||
|
throw new std::runtime_error("An attempt to send a request in a wrong state");
|
||||||
|
|
||||||
|
initializeRequest();
|
||||||
|
aquireRequest(manager);
|
||||||
|
connect(reply.get(), &QNetworkReply::finished, this, &Request::onFinished, Qt::QueuedConnection);
|
||||||
|
state = State::sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Request::cancel () {
|
||||||
|
if (stateTerminal())
|
||||||
|
return;
|
||||||
|
|
||||||
|
state = State::cancelled;
|
||||||
|
if (reply)
|
||||||
|
reply->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Request::setAuthorizationToken (const QString& token) {
|
||||||
|
request.setRawHeader("Authorization", "Bearer " + token.toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Request::aquireRequest (QNetworkAccessManager& manager) {
|
||||||
|
reply = std::unique_ptr<QNetworkReply, NetworkReplyDeleter>(manager.get(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Request::initializeRequest () {
|
||||||
|
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QString> Request::Request::validate () {
|
||||||
|
QNetworkReply::NetworkError error = reply->error();
|
||||||
|
|
||||||
|
if (error != QNetworkReply::NoError)
|
||||||
|
return reply->errorString();
|
||||||
|
else
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QVariantMap> Request::Request::readResult () {
|
||||||
|
QVariant contentType = reply->header(QNetworkRequest::ContentTypeHeader);
|
||||||
|
if (!contentType.isValid() || !contentType.canConvert<QString>() || contentType.toString() != json)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
QByteArray data = reply->readAll();
|
||||||
|
QJsonDocument document = QJsonDocument::fromJson(data);
|
||||||
|
if (!document.isObject())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
QJsonObject object = document.object();
|
||||||
|
return object.toVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Request::onSuccess (const QVariantMap& data) {
|
||||||
|
emit success(data);
|
||||||
|
emit done();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Request::onError (const QString& err, const std::optional<QVariantMap>& data) {
|
||||||
|
emit error(err, data);
|
||||||
|
emit done();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Request::Request::validateResponse (const std::optional<QVariantMap>& data, const std::map<QString, QMetaType::Type>& structure) {
|
||||||
|
if (!data.has_value())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const std::pair<const QString, QMetaType::Type>& pair : structure) {
|
||||||
|
QVariantMap::ConstIterator itr = data->find(pair.first);
|
||||||
|
if (itr == data->end())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (itr->userType() != pair.second)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl Request::Request::createUrl (QUrl base, const QString& path) {
|
||||||
|
QString startingPath = base.path();
|
||||||
|
base.setPath(startingPath + path);
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Request::Request::stateTerminal () const {
|
||||||
|
return state == State::responded || state == State::cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Request::Request::onFinished () {
|
||||||
|
if (state != State::sent)
|
||||||
|
return;
|
||||||
|
|
||||||
|
state = State::responded;
|
||||||
|
std::optional<QString> err = validate();
|
||||||
|
std::optional<QVariantMap> data;
|
||||||
|
if (!emptyResult)
|
||||||
|
data = readResult();
|
||||||
|
|
||||||
|
if (err)
|
||||||
|
return onError(err.value(), data);
|
||||||
|
|
||||||
|
if (emptyResult)
|
||||||
|
return onSuccess({});
|
||||||
|
|
||||||
|
if (data)
|
||||||
|
return onSuccess(data.value());
|
||||||
|
else
|
||||||
|
return onError("Error reading request result", data);
|
||||||
|
}
|
69
API/requests/request.h
Normal file
69
API/requests/request.h
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariantMap>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
namespace Request {
|
||||||
|
class Request : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
Request (const QUrl& url);
|
||||||
|
virtual ~Request ();
|
||||||
|
|
||||||
|
void send (QNetworkAccessManager& manager);
|
||||||
|
void cancel ();
|
||||||
|
void setAuthorizationToken(const QString& token);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void done ();
|
||||||
|
void success (const QVariantMap& data);
|
||||||
|
void error (const QString& error, const std::optional<QVariantMap>& data);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void aquireRequest (QNetworkAccessManager& manager);
|
||||||
|
virtual void initializeRequest ();
|
||||||
|
virtual void onSuccess (const QVariantMap& data);
|
||||||
|
virtual void onError (const QString& error, const std::optional<QVariantMap>& data);
|
||||||
|
|
||||||
|
std::optional<QString> validate ();
|
||||||
|
std::optional<QVariantMap> readResult ();
|
||||||
|
|
||||||
|
static bool validateResponse (const std::optional<QVariantMap>& data, const std::map<QString, QMetaType::Type>& structure);
|
||||||
|
static QUrl createUrl(QUrl base, const QString& path);
|
||||||
|
bool stateTerminal() const;
|
||||||
|
|
||||||
|
constexpr static const char * const json = "application/json";
|
||||||
|
constexpr static const char * const urlEncoded = "application/x-www-form-urlencoded";
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onFinished ();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static const std::map<QString, QMetaType::Type> resultStructure;
|
||||||
|
enum class State {
|
||||||
|
initial,
|
||||||
|
sent,
|
||||||
|
responded,
|
||||||
|
cancelled
|
||||||
|
};
|
||||||
|
struct NetworkReplyDeleter {
|
||||||
|
void operator () (QNetworkReply* reply) {
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
State state;
|
||||||
|
QNetworkRequest request;
|
||||||
|
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> reply;
|
||||||
|
bool emptyResult;
|
||||||
|
};
|
||||||
|
}
|
27
API/requests/test.cpp
Normal file
27
API/requests/test.cpp
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "test.h"
|
||||||
|
|
||||||
|
const std::map<QString, QMetaType::Type> Request::Test::structure({
|
||||||
|
{"type", QMetaType::QString}, {"version", QMetaType::QString},
|
||||||
|
});
|
||||||
|
|
||||||
|
Request::Test::Test (const QString& path):
|
||||||
|
Request(path + "/test")
|
||||||
|
{}
|
||||||
|
|
||||||
|
void Request::Test::onSuccess (const QVariantMap& data) {
|
||||||
|
if (!validateResponse(data, structure))
|
||||||
|
return onError("Malformed response", data);
|
||||||
|
|
||||||
|
QString type = data.value("type").toString();
|
||||||
|
if (type != "pica")
|
||||||
|
return onError("server of this type (" + type + ") is not supported", data);
|
||||||
|
|
||||||
|
QString version = data.value("version").toString();
|
||||||
|
if (version != "0.0.1")
|
||||||
|
return onError("server of this version (" + version + ") is not supported", data);
|
||||||
|
|
||||||
|
Request::onSuccess(data);
|
||||||
|
}
|
23
API/requests/test.h
Normal file
23
API/requests/test.h
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "request.h"
|
||||||
|
|
||||||
|
namespace Request {
|
||||||
|
|
||||||
|
class Test : public Request {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
Test (const QString& path);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void onSuccess (const QVariantMap &data) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static const std::map<QString, QMetaType::Type> structure;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -59,7 +59,9 @@ endif()
|
|||||||
|
|
||||||
add_subdirectory(qml)
|
add_subdirectory(qml)
|
||||||
add_subdirectory(API)
|
add_subdirectory(API)
|
||||||
|
add_subdirectory(models)
|
||||||
|
|
||||||
|
target_include_directories(magpie PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(magpie PRIVATE
|
target_link_libraries(magpie PRIVATE
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::Gui
|
Qt6::Gui
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
set(HEADERS
|
set(HEADERS
|
||||||
|
magpie.h
|
||||||
assets.h
|
assets.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set(SOURCES
|
set(SOURCES
|
||||||
|
magpie.cpp
|
||||||
assets.cpp
|
assets.cpp
|
||||||
)
|
)
|
||||||
|
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
#include "assets.h"
|
#include "assets.h"
|
||||||
|
|
||||||
#include "../helpers.h"
|
#include "API/helpers.h"
|
||||||
|
|
||||||
Models::Assets::Assets (QObject* parent):
|
Models::Assets::Assets (QObject* parent):
|
||||||
QAbstractListModel(parent),
|
QAbstractListModel(parent),
|
||||||
@ -22,7 +22,7 @@ void Models::Assets::addAsset (const Asset& asset) {
|
|||||||
if (index.isValid())
|
if (index.isValid())
|
||||||
throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model");
|
throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model");
|
||||||
|
|
||||||
beginInsertRows(QModelIndex(), records.size(), records.size() + 1);
|
beginInsertRows(QModelIndex(), records.size(), records.size());
|
||||||
records.push_back(asset);
|
records.push_back(asset);
|
||||||
endInsertRows();
|
endInsertRows();
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ void Models::Assets::addAssets (const std::deque<Asset>& assets) {
|
|||||||
if (getIndex(asset.id).isValid())
|
if (getIndex(asset.id).isValid())
|
||||||
throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model (bulk)");
|
throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model (bulk)");
|
||||||
|
|
||||||
beginInsertRows(QModelIndex(), records.size(), records.size() + assets.size());
|
beginInsertRows(QModelIndex(), records.size(), records.size() + assets.size() - 1);
|
||||||
for (const Asset& asset : assets)
|
for (const Asset& asset : assets)
|
||||||
records.push_back(asset);
|
records.push_back(asset);
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ void Models::Assets::deleteAsset (unsigned int id) {
|
|||||||
throw std::runtime_error("An attempt to insert to delete non existing Asset from asset model");
|
throw std::runtime_error("An attempt to insert to delete non existing Asset from asset model");
|
||||||
|
|
||||||
int row = index.row();
|
int row = index.row();
|
||||||
beginRemoveRows(QModelIndex(), row, row + 1);
|
beginRemoveRows(QModelIndex(), row, row);
|
||||||
records.erase(records.begin() + row);
|
records.erase(records.begin() + row);
|
||||||
if (state == State::syncronized) //give a second thought
|
if (state == State::syncronized) //give a second thought
|
||||||
state = State::initial;
|
state = State::initial;
|
||||||
@ -68,7 +68,7 @@ int Models::Assets::rowCount (const QModelIndex& parent) const {
|
|||||||
|
|
||||||
QHash<int, QByteArray> Models::Assets::roleNames () const {
|
QHash<int, QByteArray> Models::Assets::roleNames () const {
|
||||||
static const QHash<int, QByteArray> roleNames{
|
static const QHash<int, QByteArray> roleNames{
|
||||||
{Title, "title"}, {Icon, "icon"}, {Balance, "balance"}, {Archived, "archived"}
|
{Title, "title"}, {Icon, "icon"}, {Balance, "balance"}, {Archived, "archived"}, {Color, "color"}
|
||||||
};
|
};
|
||||||
return roleNames;
|
return roleNames;
|
||||||
}
|
}
|
||||||
@ -101,6 +101,8 @@ QVariant Models::Assets::data (const QModelIndex& index, int role) const {
|
|||||||
return records[row].balance;
|
return records[row].balance;
|
||||||
case Archived:
|
case Archived:
|
||||||
return records[row].archived;
|
return records[row].archived;
|
||||||
|
case Color:
|
||||||
|
return records[row].color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +120,10 @@ bool Models::Assets::deserialize (const QVariantList& from, std::deque<Asset>& o
|
|||||||
asset.icon = ser.value("icon").toString();
|
asset.icon = ser.value("icon").toString();
|
||||||
asset.archived = ser.value("archived").toBool();
|
asset.archived = ser.value("archived").toBool();
|
||||||
asset.id = ser.value("id").toUInt();
|
asset.id = ser.value("id").toUInt();
|
||||||
|
|
||||||
|
uint32_t color = ser.value("color").toUInt();
|
||||||
|
uint8_t* rgba = reinterpret_cast<uint8_t*>(&color);
|
||||||
|
asset.color = QColor::fromRgb(rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -135,5 +141,6 @@ QModelIndex Models::Assets::getIndex (unsigned int id) const {
|
|||||||
if (records[i].id == id)
|
if (records[i].id == id)
|
||||||
return createIndex(i, 0, &records[i]);
|
return createIndex(i, 0, &records[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return QModelIndex();
|
return QModelIndex();
|
||||||
}
|
}
|
@ -6,6 +6,7 @@
|
|||||||
#include <deque>
|
#include <deque>
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QColor>
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <qqmlregistration.h>
|
#include <qqmlregistration.h>
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ struct Asset {
|
|||||||
unsigned int id;
|
unsigned int id;
|
||||||
QString title;
|
QString title;
|
||||||
QString icon;
|
QString icon;
|
||||||
|
QColor color;
|
||||||
double balance;
|
double balance;
|
||||||
bool archived;
|
bool archived;
|
||||||
unsigned int currency;
|
unsigned int currency;
|
||||||
@ -22,7 +24,6 @@ struct Asset {
|
|||||||
class Assets : public QAbstractListModel {
|
class Assets : public QAbstractListModel {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
QML_SINGLETON
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Assets (QObject* parent = nullptr);
|
explicit Assets (QObject* parent = nullptr);
|
||||||
@ -31,7 +32,8 @@ public:
|
|||||||
Title = Qt::UserRole + 1,
|
Title = Qt::UserRole + 1,
|
||||||
Icon,
|
Icon,
|
||||||
Balance,
|
Balance,
|
||||||
Archived
|
Archived,
|
||||||
|
Color
|
||||||
};
|
};
|
||||||
|
|
||||||
void clear();
|
void clear();
|
200
models/magpie.cpp
Normal file
200
models/magpie.cpp
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "magpie.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
#include "API/api.h"
|
||||||
|
#include "API/helpers.h"
|
||||||
|
#include "API/codes.h"
|
||||||
|
|
||||||
|
Models::Magpie::Magpie (QObject* parent):
|
||||||
|
QObject(parent),
|
||||||
|
assets(),
|
||||||
|
address(),
|
||||||
|
state(State::Offline),
|
||||||
|
accessToken(),
|
||||||
|
renewToken(),
|
||||||
|
api(),
|
||||||
|
firstPoll(),
|
||||||
|
pollRequestId(API::none)
|
||||||
|
{
|
||||||
|
firstPoll.setSingleShot(true);
|
||||||
|
firstPoll.setInterval(2000);
|
||||||
|
connect(&firstPoll, &QTimer::timeout, this, &Magpie::onFirstPollTimerSuccess);
|
||||||
|
connect(&assets, &Assets::requestAssets, this, &Magpie::requestAssets);
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl Models::Magpie::getAddress () const {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
|
||||||
|
Models::Magpie::State Models::Magpie::getState () const {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Models::Magpie::getAccessToken () const {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Models::Magpie::getRenewToken () const {
|
||||||
|
return renewToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::installAPI (const std::shared_ptr<API>& api) {
|
||||||
|
Magpie::api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::setAddress (const QUrl& address) {
|
||||||
|
if (Magpie::address == address)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (state == Authenticated) {
|
||||||
|
//do something
|
||||||
|
}
|
||||||
|
|
||||||
|
Magpie::address = address;
|
||||||
|
emit addressChanged(address);
|
||||||
|
if (address.isEmpty())
|
||||||
|
setState(NoServer);
|
||||||
|
else
|
||||||
|
setState(NotAuthenticated);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::forceAddress (const QUrl& address) {
|
||||||
|
Magpie::address = "";
|
||||||
|
setAddress(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::setTokens (const QString access, const QString& renew, bool notify) {
|
||||||
|
accessToken = access;
|
||||||
|
renewToken = renew;
|
||||||
|
setState(Authenticating);
|
||||||
|
startPolling();
|
||||||
|
|
||||||
|
if (notify)
|
||||||
|
emit storeTokens(accessToken, renewToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::setState (State newState) {
|
||||||
|
if (newState == state)
|
||||||
|
return;
|
||||||
|
|
||||||
|
state = newState;
|
||||||
|
emit stateChanged(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
Models::Assets* Models::Magpie::getAssets () {
|
||||||
|
return &assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::requestAssets () {
|
||||||
|
api->requestAssets(
|
||||||
|
[this] (const QVariantList& list) {
|
||||||
|
std::deque<Asset> result;
|
||||||
|
bool res = Assets::deserialize(list, result);
|
||||||
|
if (!res) {
|
||||||
|
qDebug() << "Error deserializer received assets";
|
||||||
|
result.clear();
|
||||||
|
} else {
|
||||||
|
qDebug() << "Assets successfully received";
|
||||||
|
}
|
||||||
|
|
||||||
|
assets.addAssets(result);
|
||||||
|
},
|
||||||
|
[this] (const QString& error, const std::optional<QVariantMap>& data) {
|
||||||
|
assets.addAssets({});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::onFirstPollTimerSuccess () {
|
||||||
|
setState(Authenticated);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::startPolling () {
|
||||||
|
qDebug() << "Starting polling...";
|
||||||
|
if (state != Authenticating)
|
||||||
|
throw std::runtime_error("Can not start polling in this state: " + std::to_string(state));
|
||||||
|
|
||||||
|
sendPoll();
|
||||||
|
firstPoll.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::sendPoll (bool clear) {
|
||||||
|
if (pollRequestId != API::none)
|
||||||
|
throw std::runtime_error("an attempt to send second poll request while the other one is active");
|
||||||
|
|
||||||
|
pollRequestId = api->poll(
|
||||||
|
std::bind(&Magpie::onPollSuccess, this, std::placeholders::_1),
|
||||||
|
std::bind(&Magpie::onPollError, this, std::placeholders::_1, std::placeholders::_2),
|
||||||
|
clear
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::onPollSuccess (const QVariantMap& data) {
|
||||||
|
pollRequestId = API::none;
|
||||||
|
firstPoll.stop();
|
||||||
|
|
||||||
|
Codes::Poll code = Codes::convertPoll(data.value("result").toInt());
|
||||||
|
bool clear = false;
|
||||||
|
switch (code) {
|
||||||
|
case Codes::Poll::success:
|
||||||
|
clear = handleChanges(qast<QVariantMap>(data.value("data")));
|
||||||
|
//todo handle the result
|
||||||
|
case Codes::Poll::timeout:
|
||||||
|
setState(Authenticated);
|
||||||
|
return sendPoll(clear);
|
||||||
|
case Codes::Poll::tokenProblem:
|
||||||
|
case Codes::Poll::replace:
|
||||||
|
case Codes::Poll::unknownError: //todo this one doesn't actually mean that we can't work for now, the network may be temporarily down or something
|
||||||
|
return setState(NotAuthenticated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::onPollError (const QString& err, const std::optional<QVariantMap>& data) {
|
||||||
|
qDebug() << "Poll error:" << err;
|
||||||
|
pollRequestId = API::none;
|
||||||
|
firstPoll.stop();
|
||||||
|
setState(NotAuthenticated);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Models::Magpie::handleChanges (const QVariantMap& changes) {
|
||||||
|
QVariantMap::ConstIterator itr = changes.constFind("system");
|
||||||
|
if (itr != changes.constEnd() && itr.value().canConvert<QVariantMap>()) {
|
||||||
|
const QVariantMap& sys = qast<QVariantMap>(itr.value());
|
||||||
|
QVariantMap::ConstIterator invItr = sys.constFind("invalidate");
|
||||||
|
if (invItr != sys.constEnd()) {
|
||||||
|
const QVariant& vinv = invItr.value();
|
||||||
|
if (vinv.canConvert<bool>() && vinv.toBool())
|
||||||
|
resetAllModels();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itr = changes.constFind("assets");
|
||||||
|
if (itr != changes.constEnd() && itr.value().canConvert<QVariantMap>()) {
|
||||||
|
const QVariantMap& assets = qast<QVariantMap>(itr.value());
|
||||||
|
QVariantMap::ConstIterator aItr = assets.constFind("invalidate");
|
||||||
|
if (aItr != assets.constEnd()) {
|
||||||
|
const QVariant& vinv = aItr.value();
|
||||||
|
if (vinv.canConvert<bool>() && vinv.toBool())
|
||||||
|
Magpie::assets.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
aItr = assets.constFind("added");
|
||||||
|
if (aItr != assets.constEnd() && aItr.value().canConvert<QVariantList>()) {
|
||||||
|
std::deque<Models::Asset> added;
|
||||||
|
if (!Models::Assets::deserialize(qast<QVariantList>(aItr.value()), added))
|
||||||
|
qDebug() << "Error deserializng added assets";
|
||||||
|
else
|
||||||
|
Magpie::assets.addAssets(added);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Models::Magpie::resetAllModels () {
|
||||||
|
assets.clear();
|
||||||
|
}
|
81
models/magpie.h
Normal file
81
models/magpie.h
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||||
|
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory.h>
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include "assets.h"
|
||||||
|
|
||||||
|
class API;
|
||||||
|
namespace Models {
|
||||||
|
|
||||||
|
class Magpie : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
|
||||||
|
public:
|
||||||
|
enum State {
|
||||||
|
Offline,
|
||||||
|
NoServer,
|
||||||
|
NotAuthenticated,
|
||||||
|
Authenticating,
|
||||||
|
Authenticated
|
||||||
|
};
|
||||||
|
Q_ENUM(State)
|
||||||
|
|
||||||
|
Q_PROPERTY(QUrl address READ getAddress NOTIFY addressChanged)
|
||||||
|
Q_PROPERTY(State state READ getState NOTIFY stateChanged)
|
||||||
|
Q_PROPERTY(Assets* assets READ getAssets CONSTANT)
|
||||||
|
|
||||||
|
explicit Magpie(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QUrl getAddress() const;
|
||||||
|
State getState() const;
|
||||||
|
QString getAccessToken() const;
|
||||||
|
QString getRenewToken() const;
|
||||||
|
|
||||||
|
void installAPI(const std::shared_ptr<API>& api);
|
||||||
|
void setAddress(const QUrl& address);
|
||||||
|
void forceAddress(const QUrl& address);
|
||||||
|
void setTokens(const QString access, const QString& renew, bool notify = false);
|
||||||
|
void setState(State newState);
|
||||||
|
Assets* getAssets();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void addressChanged(const QUrl& path);
|
||||||
|
void stateChanged(State state);
|
||||||
|
|
||||||
|
void storeTokens(const QString& access, const QString& renew);
|
||||||
|
|
||||||
|
public:
|
||||||
|
Assets assets;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void requestAssets();
|
||||||
|
void onFirstPollTimerSuccess();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void startPolling();
|
||||||
|
void sendPoll(bool clear = false);
|
||||||
|
void onPollSuccess(const QVariantMap& data);
|
||||||
|
void onPollError(const QString& err, const std::optional<QVariantMap>& data);
|
||||||
|
bool handleChanges(const QVariantMap& changes);
|
||||||
|
void resetAllModels();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QUrl address;
|
||||||
|
State state;
|
||||||
|
QString accessToken;
|
||||||
|
QString renewToken;
|
||||||
|
std::shared_ptr<API> api;
|
||||||
|
QTimer firstPoll;
|
||||||
|
unsigned int pollRequestId;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -5,7 +5,7 @@ import QtQuick
|
|||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
import magpie.Models as Models
|
import magpie
|
||||||
import magpie.Components as Components
|
import magpie.Components as Components
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@ -28,9 +28,11 @@ Item {
|
|||||||
id: listView
|
id: listView
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
model: Models.Assets
|
model: Magpie.assets
|
||||||
|
spacing: 5
|
||||||
delegate: Components.AssetLine {
|
delegate: Components.AssetLine {
|
||||||
height: 20
|
height: 20
|
||||||
|
width: listView.width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,23 +4,50 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
Rectangle {
|
Item {
|
||||||
id: line
|
id: line
|
||||||
required property string title
|
required property string title
|
||||||
required property string icon
|
required property string icon
|
||||||
|
required property color color
|
||||||
|
required property string balance
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
readonly property int iconSize: height
|
||||||
|
readonly property int freespace: width - iconSize - spacing * children.length - 1
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
IconLabel {
|
spacing: 5
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
icon.name: line.icon
|
Rectangle {
|
||||||
width: parent.height
|
width: parent.iconSize
|
||||||
height: parent.height
|
height: parent.iconSize
|
||||||
|
color: line.color
|
||||||
|
|
||||||
|
IconLabel {
|
||||||
|
anchors.fill: parent
|
||||||
|
icon {
|
||||||
|
name: line.icon
|
||||||
|
width: width
|
||||||
|
height: height
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
width: parent.freespace / 2
|
||||||
|
height: parent.height
|
||||||
text: title
|
text: title
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
color: palette.text
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.freespace / 2
|
||||||
|
height: parent.height
|
||||||
|
text: balance
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
color: palette.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import magpie.API
|
import magpie
|
||||||
import magpie.Components as Components
|
import magpie.Components as Components
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import magpie.API
|
import magpie
|
||||||
import magpie.Components as Components
|
import magpie.Components as Components
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
|
|
||||||
import magpie.API
|
import magpie
|
||||||
import magpie.Components as Components
|
import magpie.Components as Components
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
@ -5,7 +5,7 @@ import QtQuick
|
|||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
import magpie.API
|
import magpie
|
||||||
import magpie.Components as Components
|
import magpie.Components as Components
|
||||||
|
|
||||||
Page {
|
Page {
|
||||||
|
@ -5,7 +5,7 @@ import QtQuick
|
|||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
import magpie.API
|
import magpie
|
||||||
import magpie.Forms as Forms
|
import magpie.Forms as Forms
|
||||||
import magpie.Components as Components
|
import magpie.Components as Components
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ Page {
|
|||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
horizontalAlignment: Label.AlignLeft
|
horizontalAlignment: Label.AlignLeft
|
||||||
text: API.state === API.NoServer ? qsTr("choose") : API.address
|
text: Magpie.state === Magpie.NoServer ? qsTr("choose") : Magpie.address
|
||||||
font {
|
font {
|
||||||
italic: true
|
italic: true
|
||||||
underline: true
|
underline: true
|
||||||
@ -52,20 +52,20 @@ Page {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: pickServer(API.address)
|
onClicked: pickServer(Magpie.address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Components.Modal {
|
Components.Modal {
|
||||||
inProgress: API.state === API.Authenticating
|
inProgress: Magpie.state === Magpie.Authenticating
|
||||||
visible: !priv.loggingIn && API.state === API.Authenticating
|
visible: !priv.loggingIn && Magpie.state === Magpie.Authenticating
|
||||||
closable: API.state === API.NotAuthenticated
|
closable: Magpie.state === Magpie.NotAuthenticated
|
||||||
status: "Logging into " + API.address + "..."
|
status: "Logging into " + Magpie.address + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
visible: priv.loggingIn || API.state === API.NotAuthenticated || API.state === API.Authenticating
|
visible: priv.loggingIn || Magpie.state === Magpie.NotAuthenticated || Magpie.state === Magpie.Authenticating
|
||||||
|
|
||||||
width: page.width
|
width: page.width
|
||||||
height: stack.currentItem ? stack.currentItem.implicitHeight: 0
|
height: stack.currentItem ? stack.currentItem.implicitHeight: 0
|
||||||
|
10
qml/main.qml
10
qml/main.qml
@ -7,7 +7,7 @@ import QtQuick.Controls
|
|||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtCore
|
import QtCore
|
||||||
|
|
||||||
import magpie.API
|
import magpie
|
||||||
import magpie.Application as Application
|
import magpie.Application as Application
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
@ -35,7 +35,7 @@ ApplicationWindow {
|
|||||||
Component {
|
Component {
|
||||||
id: serverPick
|
id: serverPick
|
||||||
ServerPick {
|
ServerPick {
|
||||||
address: API.address
|
address: Magpie.address
|
||||||
onBack: stack.pop()
|
onBack: stack.pop()
|
||||||
StackView.onActivating: pickingServer = true;
|
StackView.onActivating: pickingServer = true;
|
||||||
StackView.onDeactivating: pickingServer = false;
|
StackView.onDeactivating: pickingServer = false;
|
||||||
@ -52,15 +52,15 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: API
|
target: Magpie
|
||||||
function onAddressChanged (url) {
|
function onAddressChanged (url) {
|
||||||
if (pickingServer && url.toString().length > 0)
|
if (pickingServer && url.toString().length > 0)
|
||||||
stack.pop()
|
stack.pop()
|
||||||
}
|
}
|
||||||
function onStateChanged (state) {
|
function onStateChanged (state) {
|
||||||
if (state === API.Authenticated)
|
if (state === Magpie.Authenticated)
|
||||||
stack.push(app);
|
stack.push(app);
|
||||||
else if (runningApp && state === API.NotAuthenticated)
|
else if (runningApp && state === Magpie.NotAuthenticated)
|
||||||
stack.pop();
|
stack.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
root.cpp
21
root.cpp
@ -10,7 +10,8 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) :
|
|||||||
root(root),
|
root(root),
|
||||||
engine(),
|
engine(),
|
||||||
context(engine.rootContext()),
|
context(engine.rootContext()),
|
||||||
api()
|
magpie(),
|
||||||
|
api(std::make_shared<API>(magpie))
|
||||||
{
|
{
|
||||||
std::cout << "Starting Magpie..." << std::endl;
|
std::cout << "Starting Magpie..." << std::endl;
|
||||||
|
|
||||||
@ -21,26 +22,26 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) :
|
|||||||
setApplicationVersion("0.0.1");
|
setApplicationVersion("0.0.1");
|
||||||
setDesktopFileName("magpie");
|
setDesktopFileName("magpie");
|
||||||
|
|
||||||
|
magpie.installAPI(api);
|
||||||
|
|
||||||
connect(&engine, &QQmlApplicationEngine::objectCreated,
|
connect(&engine, &QQmlApplicationEngine::objectCreated,
|
||||||
this, &Root::onObjectCreated,
|
this, &Root::onObjectCreated,
|
||||||
Qt::QueuedConnection
|
Qt::QueuedConnection
|
||||||
);
|
);
|
||||||
|
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
api.setAddress(settings.value("address").toUrl());
|
magpie.setAddress(settings.value("address").toUrl());
|
||||||
|
|
||||||
QString acc = settings.value("accessToken").toString();
|
QString acc = settings.value("accessToken").toString();
|
||||||
QString ren = settings.value("renewToken").toString();
|
QString ren = settings.value("renewToken").toString();
|
||||||
if (!acc.isEmpty() && !ren.isEmpty())
|
if (!acc.isEmpty() && !ren.isEmpty())
|
||||||
api.setTokens(acc, ren);
|
magpie.setTokens(acc, ren);
|
||||||
|
|
||||||
qRegisterMetaType<API>("API");
|
connect(&magpie, &Models::Magpie::addressChanged, this, &Root::onAPIAddressChanged);
|
||||||
connect(&api, &API::addressChanged, this, &Root::onAPIAddressChanged);
|
connect(&magpie, &Models::Magpie::storeTokens, this, &Root::onStoreTokens);
|
||||||
connect(&api, &API::storeTokens, this, &Root::onStoreTokens);
|
|
||||||
|
|
||||||
qmlRegisterSingletonInstance("magpie.API", 1, 0, "API", &api);
|
qmlRegisterSingletonInstance("magpie", 1, 0, "API", api.get());
|
||||||
|
qmlRegisterSingletonInstance("magpie", 1, 0, "Magpie", &magpie);
|
||||||
qmlRegisterSingletonInstance("magpie.Models", 1, 0, "Assets", &api.assets);
|
|
||||||
|
|
||||||
engine.addImportPath(":/");
|
engine.addImportPath(":/");
|
||||||
engine.load(root);
|
engine.load(root);
|
||||||
@ -64,7 +65,7 @@ bool Root::notify(QObject* receiver, QEvent* e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Magpie is quiting..." << std::endl;
|
std::cout << "Magpie is quiting..." << std::endl;
|
||||||
exit(1);
|
exit(-1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
root.h
6
root.h
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
@ -12,6 +13,7 @@
|
|||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
|
|
||||||
|
#include "models/magpie.h"
|
||||||
#include "API/api.h"
|
#include "API/api.h"
|
||||||
|
|
||||||
class Root : public QGuiApplication {
|
class Root : public QGuiApplication {
|
||||||
@ -30,5 +32,7 @@ private:
|
|||||||
QUrl root;
|
QUrl root;
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
QQmlContext* context;
|
QQmlContext* context;
|
||||||
API api;
|
Models::Magpie magpie;
|
||||||
|
std::shared_ptr<API> api;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user