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})
|
||||
|
||||
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-License-Identifier: GPL-3.0-or-later
|
||||
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "api.h"
|
||||
#include <QDebug>
|
||||
@ -7,466 +7,128 @@
|
||||
#include <QJsonObject>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include "codes.h"
|
||||
#include "finalaction.h"
|
||||
#include "helpers.h"
|
||||
#include "requests/test.h"
|
||||
#include "requests/register.h"
|
||||
#include "requests/login.h"
|
||||
#include "requests/poll.h"
|
||||
#include "requests/listassets.h"
|
||||
#include "requests/addasset.h"
|
||||
|
||||
constexpr const char* json = "application/json";
|
||||
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):
|
||||
API::API (Models::Magpie& magpie, QObject* parent):
|
||||
QObject(parent),
|
||||
address(address),
|
||||
idCounter(0),
|
||||
magpie(magpie),
|
||||
network(),
|
||||
state(NoServer),
|
||||
accessToken(),
|
||||
renewToken(),
|
||||
firstPoll(),
|
||||
pollReply(),
|
||||
assets()
|
||||
{
|
||||
firstPoll.setSingleShot(true);
|
||||
firstPoll.setInterval(2000);
|
||||
connect(&firstPoll, &QTimer::timeout, this, &API::onFirstPollSuccess);
|
||||
connect(&assets, &Models::Assets::requestAssets, this, &API::requestAssets);
|
||||
requests()
|
||||
{}
|
||||
|
||||
void API::cancelRequest (RequestId id) {
|
||||
requests.erase(id);
|
||||
}
|
||||
|
||||
QUrl API::getAddress() const {
|
||||
return address;
|
||||
void API::onRequestDone (RequestId id) {
|
||||
requests.erase(id);
|
||||
}
|
||||
|
||||
API::State API::getState() const {
|
||||
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) {
|
||||
API::RequestId API::test (const QString& path, const QJSValue& finished) {
|
||||
qDebug() << "Testing" << path;
|
||||
if (state == Offline)
|
||||
return callCallback(finished, "Need to be online to test");
|
||||
if (magpie.getState() == Models::Magpie::Offline)
|
||||
callCallback(finished, "Need to be online to test"), 0;
|
||||
|
||||
QUrl address(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));
|
||||
std::unique_ptr<Request::Test> test = std::make_unique<Request::Test>(path);
|
||||
connect(
|
||||
pollReply.get(), &QNetworkReply::finished,
|
||||
this, &API::onPollFinished, Qt::QueuedConnection
|
||||
test.get(),
|
||||
&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() {
|
||||
firstPoll.stop();
|
||||
QNetworkReply::NetworkError error = pollReply->error();
|
||||
std::optional<QVariantMap> data = readResult(pollReply.get());
|
||||
Codes::Poll code = Codes::Poll::unknownError;
|
||||
if (validateResponse(data, resultStructure))
|
||||
code = Codes::convertPoll(data->value("result").toInt());
|
||||
else
|
||||
qDebug() << "";
|
||||
API::RequestId API::sendRegister (const QString& login, const QString& password, const QJSValue& finished) {
|
||||
qDebug() << "Registering...";
|
||||
if (magpie.getState() != Models::Magpie::NotAuthenticated)
|
||||
return callCallback(finished, "Can not register in current state"), 0;
|
||||
|
||||
QString detail = Codes::description(code);
|
||||
|
||||
if (error != QNetworkReply::NoError)
|
||||
qDebug() << pollReply->errorString() + ": " + detail;
|
||||
else
|
||||
qDebug() << "Poll finished: " + detail;
|
||||
|
||||
bool clear = false;
|
||||
switch (code) {
|
||||
case Codes::Poll::success: {
|
||||
QVariantMap::ConstIterator itr = data->constFind("data");
|
||||
if (itr == data->constEnd())
|
||||
qDebug("received a poll that claimed to have some updates, but the \"data\" field is abscent");
|
||||
|
||||
const QVariant& vdata = itr.value();
|
||||
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:
|
||||
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
|
||||
pollReply.reset();
|
||||
return setState(NotAuthenticated);
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
API::RequestId API::sendLogin (const QString& login, const QString& password, const QJSValue& finished) {
|
||||
qDebug() << "Logging in...";
|
||||
if (magpie.getState() != Models::Magpie::NotAuthenticated)
|
||||
return callCallback(finished, "Can not register in current state"), 0;
|
||||
|
||||
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())
|
||||
API::assets.clear();
|
||||
std::unique_ptr<Request::Login> log = std::make_unique<Request::Login>(login, password, magpie.getAddress());
|
||||
connect(
|
||||
log.get(),
|
||||
&Request::Login::success,
|
||||
this,
|
||||
[this, &finished] (const QVariantMap& data) {
|
||||
callCallback(finished, QString(), {QJSValue(true)});
|
||||
magpie.setTokens(data.value("accessToken").toString(), data.value("renewToken").toString());
|
||||
}
|
||||
);
|
||||
connect(
|
||||
log.get(),
|
||||
&Request::Login::error,
|
||||
this,
|
||||
[this, &finished] (const QString& error) {
|
||||
callCallback(finished, error, {QJSValue(false)});
|
||||
magpie.setState(Models::Magpie::NotAuthenticated);
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
||||
magpie.setState(Models::Magpie::Authenticating);
|
||||
return registerAndSend(std::move(log));
|
||||
}
|
||||
|
||||
void API::resetAllModels() {
|
||||
assets.clear();
|
||||
API::RequestId API::addAsset (const QString& title, const QString& icon, const QJSValue& finished) {
|
||||
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() {
|
||||
setState(Authenticated);
|
||||
}
|
||||
|
||||
void API::requestAssets() {
|
||||
if (state != Authenticated) {
|
||||
qDebug() << "An attempt to request assets on unauthenticated state";
|
||||
assets.receivedAssets({});
|
||||
return;
|
||||
}
|
||||
API::RequestId API::requestAssets (const SuccessListHandler& success, const ErrorHandler& error) {
|
||||
qDebug() << "Requesting assets...";
|
||||
|
||||
QUrl url = createUrl("/listAssets");
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
|
||||
request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8());
|
||||
|
||||
QNetworkReply* reply = network.get(request);
|
||||
connect(
|
||||
reply, &QNetworkReply::finished,
|
||||
this, std::bind(&API::responseAssets, this, reply)
|
||||
);
|
||||
auto list = std::make_unique<Request::ListAssets>(magpie.getAddress());
|
||||
list->setAuthorizationToken(magpie.getAccessToken());
|
||||
connect(list.get(), &Request::ListAssets::success, success);
|
||||
connect(list.get(), &Request::ListAssets::error, error);
|
||||
return registerAndSend(std::move(list));
|
||||
}
|
||||
|
||||
void API::responseAssets(QNetworkReply *reply) {
|
||||
std::deque<Models::Asset> result;
|
||||
FinalAction action([this, &result]() {
|
||||
assets.receivedAssets(result);
|
||||
});
|
||||
|
||||
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
|
||||
API::RequestId API::poll (const SuccessMapHandler& success, const ErrorHandler& error, bool clear) {
|
||||
auto poll = std::make_unique<Request::Poll>(magpie.getAddress(), clear);
|
||||
poll->setAuthorizationToken(magpie.getAccessToken());
|
||||
connect(poll.get(), &Request::Poll::success, success);
|
||||
connect(poll.get(), &Request::Poll::error, error);
|
||||
return registerAndSend(std::move(poll));
|
||||
}
|
||||
|
||||
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 (error.isEmpty())
|
||||
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)});
|
||||
}
|
||||
}
|
||||
|
||||
|
78
API/api.h
78
API/api.h
@ -6,6 +6,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
@ -13,80 +14,45 @@
|
||||
#include <QJSValue>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
|
||||
#include "models/assets.h"
|
||||
#include "models/magpie.h"
|
||||
#include "requests/request.h"
|
||||
|
||||
class Root;
|
||||
class API : public QObject {
|
||||
friend class Root;
|
||||
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:
|
||||
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;
|
||||
State getState() const;
|
||||
void setTokens(const QString access, const QString& renew);
|
||||
void startPolling();
|
||||
explicit API(Models::Magpie& magpie, QObject* parent = nullptr);
|
||||
|
||||
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:
|
||||
void test(const QString& path, const QJSValue& finished = QJSValue());
|
||||
void sendRegister(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
|
||||
void sendLogin(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
|
||||
void addAsset(const QString& title, const QString& icon, const QJSValue& finished = QJSValue());
|
||||
void cancelRequest(RequestId id);
|
||||
RequestId test(const QString& path, const QJSValue& finished = QJSValue());
|
||||
RequestId sendRegister(const QString& login, const QString& password, 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:
|
||||
void onTestFinished(QNetworkReply* reply, const QUrl& addr, const QJSValue& finished);
|
||||
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);
|
||||
void onRequestDone(RequestId id);
|
||||
|
||||
private:
|
||||
void callCallback(const QJSValue& callback, const QString& error = QString(), const QJSValueList& arguments = QJSValueList()) const;
|
||||
void setAddress(const QUrl& path);
|
||||
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();
|
||||
RequestId registerAndSend(std::unique_ptr<Request::Request> request);
|
||||
|
||||
private:
|
||||
QUrl address;
|
||||
RequestId idCounter;
|
||||
Models::Magpie& magpie;
|
||||
QNetworkAccessManager network;
|
||||
State state;
|
||||
QString accessToken;
|
||||
QString renewToken;
|
||||
QTimer firstPoll;
|
||||
std::map<RequestId, std::unique_ptr<Request::Request>> requests;
|
||||
std::unique_ptr<QNetworkReply> pollReply;
|
||||
|
||||
public:
|
||||
Models::Assets assets;
|
||||
};
|
||||
|
@ -6,6 +6,8 @@
|
||||
#include <QVariant>
|
||||
#include <stdexcept>
|
||||
|
||||
#define UNUSED(X) (void)(X)
|
||||
|
||||
template <class T>
|
||||
const T& qast(const QVariant& variant) {
|
||||
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(API)
|
||||
add_subdirectory(models)
|
||||
|
||||
target_include_directories(magpie PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(magpie PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
|
@ -2,10 +2,12 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
set(HEADERS
|
||||
magpie.h
|
||||
assets.h
|
||||
)
|
||||
|
||||
set(SOURCES
|
||||
magpie.cpp
|
||||
assets.cpp
|
||||
)
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
#include "assets.h"
|
||||
|
||||
#include "../helpers.h"
|
||||
#include "API/helpers.h"
|
||||
|
||||
Models::Assets::Assets (QObject* parent):
|
||||
QAbstractListModel(parent),
|
||||
@ -22,7 +22,7 @@ void Models::Assets::addAsset (const Asset& asset) {
|
||||
if (index.isValid())
|
||||
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);
|
||||
endInsertRows();
|
||||
}
|
||||
@ -35,7 +35,7 @@ void Models::Assets::addAssets (const std::deque<Asset>& assets) {
|
||||
if (getIndex(asset.id).isValid())
|
||||
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)
|
||||
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");
|
||||
|
||||
int row = index.row();
|
||||
beginRemoveRows(QModelIndex(), row, row + 1);
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
records.erase(records.begin() + row);
|
||||
if (state == State::syncronized) //give a second thought
|
||||
state = State::initial;
|
||||
@ -68,7 +68,7 @@ int Models::Assets::rowCount (const QModelIndex& parent) const {
|
||||
|
||||
QHash<int, QByteArray> Models::Assets::roleNames () const {
|
||||
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;
|
||||
}
|
||||
@ -101,6 +101,8 @@ QVariant Models::Assets::data (const QModelIndex& index, int role) const {
|
||||
return records[row].balance;
|
||||
case 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.archived = ser.value("archived").toBool();
|
||||
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;
|
||||
@ -135,5 +141,6 @@ QModelIndex Models::Assets::getIndex (unsigned int id) const {
|
||||
if (records[i].id == id)
|
||||
return createIndex(i, 0, &records[i]);
|
||||
}
|
||||
|
||||
return QModelIndex();
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
#include <deque>
|
||||
|
||||
#include <QString>
|
||||
#include <QColor>
|
||||
#include <QAbstractListModel>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
@ -14,6 +15,7 @@ struct Asset {
|
||||
unsigned int id;
|
||||
QString title;
|
||||
QString icon;
|
||||
QColor color;
|
||||
double balance;
|
||||
bool archived;
|
||||
unsigned int currency;
|
||||
@ -22,7 +24,6 @@ struct Asset {
|
||||
class Assets : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
public:
|
||||
explicit Assets (QObject* parent = nullptr);
|
||||
@ -31,7 +32,8 @@ public:
|
||||
Title = Qt::UserRole + 1,
|
||||
Icon,
|
||||
Balance,
|
||||
Archived
|
||||
Archived,
|
||||
Color
|
||||
};
|
||||
|
||||
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.Layouts
|
||||
|
||||
import magpie.Models as Models
|
||||
import magpie
|
||||
import magpie.Components as Components
|
||||
|
||||
Item {
|
||||
@ -28,9 +28,11 @@ Item {
|
||||
id: listView
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
model: Models.Assets
|
||||
model: Magpie.assets
|
||||
spacing: 5
|
||||
delegate: Components.AssetLine {
|
||||
height: 20
|
||||
width: listView.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,23 +4,50 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Rectangle {
|
||||
Item {
|
||||
id: line
|
||||
required property string title
|
||||
required property string icon
|
||||
required property color color
|
||||
required property string balance
|
||||
|
||||
Row {
|
||||
readonly property int iconSize: height
|
||||
readonly property int freespace: width - iconSize - spacing * children.length - 1
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
Rectangle {
|
||||
width: parent.iconSize
|
||||
height: parent.iconSize
|
||||
color: line.color
|
||||
|
||||
IconLabel {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
icon.name: line.icon
|
||||
width: parent.height
|
||||
height: parent.height
|
||||
anchors.fill: parent
|
||||
icon {
|
||||
name: line.icon
|
||||
width: width
|
||||
height: height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.freespace / 2
|
||||
height: parent.height
|
||||
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.Controls
|
||||
|
||||
import magpie.API
|
||||
import magpie
|
||||
import magpie.Components as Components
|
||||
|
||||
Item {
|
||||
|
@ -4,7 +4,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import magpie.API
|
||||
import magpie
|
||||
import magpie.Components as Components
|
||||
|
||||
Column {
|
||||
|
@ -4,7 +4,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import magpie.API
|
||||
import magpie
|
||||
import magpie.Components as Components
|
||||
|
||||
Column {
|
||||
|
@ -5,7 +5,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import magpie.API
|
||||
import magpie
|
||||
import magpie.Components as Components
|
||||
|
||||
Page {
|
||||
|
@ -5,7 +5,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import magpie.API
|
||||
import magpie
|
||||
import magpie.Forms as Forms
|
||||
import magpie.Components as Components
|
||||
|
||||
@ -44,7 +44,7 @@ Page {
|
||||
}
|
||||
Label {
|
||||
horizontalAlignment: Label.AlignLeft
|
||||
text: API.state === API.NoServer ? qsTr("choose") : API.address
|
||||
text: Magpie.state === Magpie.NoServer ? qsTr("choose") : Magpie.address
|
||||
font {
|
||||
italic: true
|
||||
underline: true
|
||||
@ -52,20 +52,20 @@ Page {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: pickServer(API.address)
|
||||
onClicked: pickServer(Magpie.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Components.Modal {
|
||||
inProgress: API.state === API.Authenticating
|
||||
visible: !priv.loggingIn && API.state === API.Authenticating
|
||||
closable: API.state === API.NotAuthenticated
|
||||
status: "Logging into " + API.address + "..."
|
||||
inProgress: Magpie.state === Magpie.Authenticating
|
||||
visible: !priv.loggingIn && Magpie.state === Magpie.Authenticating
|
||||
closable: Magpie.state === Magpie.NotAuthenticated
|
||||
status: "Logging into " + Magpie.address + "..."
|
||||
}
|
||||
|
||||
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
|
||||
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 QtCore
|
||||
|
||||
import magpie.API
|
||||
import magpie
|
||||
import magpie.Application as Application
|
||||
|
||||
ApplicationWindow {
|
||||
@ -35,7 +35,7 @@ ApplicationWindow {
|
||||
Component {
|
||||
id: serverPick
|
||||
ServerPick {
|
||||
address: API.address
|
||||
address: Magpie.address
|
||||
onBack: stack.pop()
|
||||
StackView.onActivating: pickingServer = true;
|
||||
StackView.onDeactivating: pickingServer = false;
|
||||
@ -52,15 +52,15 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: API
|
||||
target: Magpie
|
||||
function onAddressChanged (url) {
|
||||
if (pickingServer && url.toString().length > 0)
|
||||
stack.pop()
|
||||
}
|
||||
function onStateChanged (state) {
|
||||
if (state === API.Authenticated)
|
||||
if (state === Magpie.Authenticated)
|
||||
stack.push(app);
|
||||
else if (runningApp && state === API.NotAuthenticated)
|
||||
else if (runningApp && state === Magpie.NotAuthenticated)
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
21
root.cpp
21
root.cpp
@ -10,7 +10,8 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) :
|
||||
root(root),
|
||||
engine(),
|
||||
context(engine.rootContext()),
|
||||
api()
|
||||
magpie(),
|
||||
api(std::make_shared<API>(magpie))
|
||||
{
|
||||
std::cout << "Starting Magpie..." << std::endl;
|
||||
|
||||
@ -21,26 +22,26 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) :
|
||||
setApplicationVersion("0.0.1");
|
||||
setDesktopFileName("magpie");
|
||||
|
||||
magpie.installAPI(api);
|
||||
|
||||
connect(&engine, &QQmlApplicationEngine::objectCreated,
|
||||
this, &Root::onObjectCreated,
|
||||
Qt::QueuedConnection
|
||||
);
|
||||
|
||||
QSettings settings;
|
||||
api.setAddress(settings.value("address").toUrl());
|
||||
magpie.setAddress(settings.value("address").toUrl());
|
||||
|
||||
QString acc = settings.value("accessToken").toString();
|
||||
QString ren = settings.value("renewToken").toString();
|
||||
if (!acc.isEmpty() && !ren.isEmpty())
|
||||
api.setTokens(acc, ren);
|
||||
magpie.setTokens(acc, ren);
|
||||
|
||||
qRegisterMetaType<API>("API");
|
||||
connect(&api, &API::addressChanged, this, &Root::onAPIAddressChanged);
|
||||
connect(&api, &API::storeTokens, this, &Root::onStoreTokens);
|
||||
connect(&magpie, &Models::Magpie::addressChanged, this, &Root::onAPIAddressChanged);
|
||||
connect(&magpie, &Models::Magpie::storeTokens, this, &Root::onStoreTokens);
|
||||
|
||||
qmlRegisterSingletonInstance("magpie.API", 1, 0, "API", &api);
|
||||
|
||||
qmlRegisterSingletonInstance("magpie.Models", 1, 0, "Assets", &api.assets);
|
||||
qmlRegisterSingletonInstance("magpie", 1, 0, "API", api.get());
|
||||
qmlRegisterSingletonInstance("magpie", 1, 0, "Magpie", &magpie);
|
||||
|
||||
engine.addImportPath(":/");
|
||||
engine.load(root);
|
||||
@ -64,7 +65,7 @@ bool Root::notify(QObject* receiver, QEvent* e) {
|
||||
}
|
||||
|
||||
std::cout << "Magpie is quiting..." << std::endl;
|
||||
exit(1);
|
||||
exit(-1);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
6
root.h
6
root.h
@ -5,6 +5,7 @@
|
||||
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <memory>
|
||||
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
@ -12,6 +13,7 @@
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
|
||||
#include "models/magpie.h"
|
||||
#include "API/api.h"
|
||||
|
||||
class Root : public QGuiApplication {
|
||||
@ -30,5 +32,7 @@ private:
|
||||
QUrl root;
|
||||
QQmlApplicationEngine engine;
|
||||
QQmlContext* context;
|
||||
API api;
|
||||
Models::Magpie magpie;
|
||||
std::shared_ptr<API> api;
|
||||
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user