big refactoring of API system
This commit is contained in:
parent
27124380e4
commit
7a116bfdf2
37 changed files with 1060 additions and 534 deletions
14
models/CMakeLists.txt
Normal file
14
models/CMakeLists.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
# SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
set(HEADERS
|
||||
magpie.h
|
||||
assets.h
|
||||
)
|
||||
|
||||
set(SOURCES
|
||||
magpie.cpp
|
||||
assets.cpp
|
||||
)
|
||||
|
||||
target_sources(magpie PRIVATE ${SOURCES})
|
146
models/assets.cpp
Normal file
146
models/assets.cpp
Normal file
|
@ -0,0 +1,146 @@
|
|||
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "assets.h"
|
||||
|
||||
#include "API/helpers.h"
|
||||
|
||||
Models::Assets::Assets (QObject* parent):
|
||||
QAbstractListModel(parent),
|
||||
records(),
|
||||
state(State::initial)
|
||||
{}
|
||||
|
||||
void Models::Assets::clear () {
|
||||
beginResetModel();
|
||||
records.clear();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void Models::Assets::addAsset (const Asset& asset) {
|
||||
QModelIndex index = getIndex(asset.id);
|
||||
if (index.isValid())
|
||||
throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model");
|
||||
|
||||
beginInsertRows(QModelIndex(), records.size(), records.size());
|
||||
records.push_back(asset);
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void Models::Assets::addAssets (const std::deque<Asset>& assets) {
|
||||
if (assets.empty())
|
||||
return;
|
||||
|
||||
for (const Asset& 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() - 1);
|
||||
for (const Asset& asset : assets)
|
||||
records.push_back(asset);
|
||||
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
void Models::Assets::deleteAsset (unsigned int id) {
|
||||
QModelIndex index = getIndex(id);
|
||||
if (!index.isValid())
|
||||
throw std::runtime_error("An attempt to insert to delete non existing Asset from asset model");
|
||||
|
||||
int row = index.row();
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
records.erase(records.begin() + row);
|
||||
if (state == State::syncronized) //give a second thought
|
||||
state = State::initial;
|
||||
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
int Models::Assets::rowCount (const QModelIndex& parent) const {
|
||||
//For list models only the root node (an invalid parent) should return the
|
||||
//list's size. For all other (valid) parents, rowCount() should return 0 so
|
||||
//that it does not become a tree model.
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
|
||||
return records.size();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> Models::Assets::roleNames () const {
|
||||
static const QHash<int, QByteArray> roleNames{
|
||||
{Title, "title"}, {Icon, "icon"}, {Balance, "balance"}, {Archived, "archived"}, {Color, "color"}
|
||||
};
|
||||
return roleNames;
|
||||
}
|
||||
|
||||
bool Models::Assets::canFetchMore (const QModelIndex& parent) const {
|
||||
return state == State::initial;
|
||||
}
|
||||
|
||||
void Models::Assets::fetchMore (const QModelIndex& parent) {
|
||||
if (state != State::initial)
|
||||
return;
|
||||
|
||||
state = State::requesting;
|
||||
emit requestAssets();
|
||||
}
|
||||
|
||||
QVariant Models::Assets::data (const QModelIndex& index, int role) const {
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
int row = index.row();
|
||||
if (row >= 0 && row < records.size()) {
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
case Title:
|
||||
return records[row].title;
|
||||
case Icon:
|
||||
return records[row].icon;
|
||||
case Balance:
|
||||
return records[row].balance;
|
||||
case Archived:
|
||||
return records[row].archived;
|
||||
case Color:
|
||||
return records[row].color;
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
bool Models::Assets::deserialize (const QVariantList& from, std::deque<Asset>& out) {
|
||||
for (const QVariant& item : from) {
|
||||
if (!item.canConvert<QVariantMap>())
|
||||
return false;
|
||||
|
||||
const QVariantMap& ser = qast<QVariantMap>(item);
|
||||
Asset& asset = out.emplace_back();
|
||||
asset.title = ser.value("title").toString();
|
||||
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;
|
||||
}
|
||||
|
||||
void Models::Assets::receivedAssets (const std::deque<Asset>& assets) {
|
||||
beginResetModel();
|
||||
records = assets;
|
||||
state = State::syncronized;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QModelIndex Models::Assets::getIndex (unsigned int id) const {
|
||||
for (std::size_t i = 0; i < records.size(); ++i) {
|
||||
if (records[i].id == id)
|
||||
return createIndex(i, 0, &records[i]);
|
||||
}
|
||||
|
||||
return QModelIndex();
|
||||
}
|
74
models/assets.h
Normal file
74
models/assets.h
Normal file
|
@ -0,0 +1,74 @@
|
|||
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
||||
//SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
|
||||
#include <QString>
|
||||
#include <QColor>
|
||||
#include <QAbstractListModel>
|
||||
#include <qqmlregistration.h>
|
||||
|
||||
namespace Models {
|
||||
struct Asset {
|
||||
unsigned int id;
|
||||
QString title;
|
||||
QString icon;
|
||||
QColor color;
|
||||
double balance;
|
||||
bool archived;
|
||||
unsigned int currency;
|
||||
};
|
||||
|
||||
class Assets : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
public:
|
||||
explicit Assets (QObject* parent = nullptr);
|
||||
|
||||
enum Roles {
|
||||
Title = Qt::UserRole + 1,
|
||||
Icon,
|
||||
Balance,
|
||||
Archived,
|
||||
Color
|
||||
};
|
||||
|
||||
void clear();
|
||||
void addAsset(const Asset& asset);
|
||||
void addAssets(const std::deque<Asset>& assets);
|
||||
void deleteAsset(unsigned int id);
|
||||
|
||||
//Basic functionality:
|
||||
int rowCount (const QModelIndex& parent = QModelIndex()) const override;
|
||||
QHash<int, QByteArray> roleNames () const override;
|
||||
|
||||
//Fetch data dynamically:
|
||||
bool canFetchMore (const QModelIndex& parent) const override;
|
||||
void fetchMore (const QModelIndex& parent) override;
|
||||
QVariant data (const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
static bool deserialize(const QVariantList& from, std::deque<Asset>& out);
|
||||
|
||||
signals:
|
||||
void requestAssets();
|
||||
|
||||
public slots:
|
||||
void receivedAssets(const std::deque<Asset>& assets);
|
||||
|
||||
private:
|
||||
QModelIndex getIndex(unsigned int id) const;
|
||||
|
||||
private:
|
||||
enum class State {
|
||||
initial,
|
||||
requesting,
|
||||
syncronized
|
||||
};
|
||||
|
||||
State state;
|
||||
std::deque<Asset> records;
|
||||
};
|
||||
}
|
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;
|
||||
};
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue