diff --git a/API/api.cpp b/API/api.cpp index 2ee2afc..fca3177 100644 --- a/API/api.cpp +++ b/API/api.cpp @@ -129,6 +129,15 @@ API::RequestId API::addAsset ( return registerAndSend(std::move(add)); } +API::RequestId API::updateAsset ( + Models::Asset::ID id, + const QString& title, + const QString& icon, + const QColor& color, + Models::Currency::ID currency, + const QJSValue& finished +) {} + API::RequestId API::deleteAsset (unsigned int id, const QJSValue& finished) { qDebug() << "Deleting asset..."; if (magpie.getState() != Models::Magpie::Authenticated) diff --git a/API/requests/CMakeLists.txt b/API/requests/CMakeLists.txt index 92a9b05..e3557ce 100644 --- a/API/requests/CMakeLists.txt +++ b/API/requests/CMakeLists.txt @@ -12,6 +12,7 @@ set(HEADERS currencies.h addasset.h deleteasset.h + updateasset.h ) set(SOURCES @@ -25,6 +26,7 @@ set(SOURCES currencies.cpp addasset.cpp deleteasset.cpp + updateasset.cpp ) target_sources(magpie PRIVATE ${SOURCES}) diff --git a/API/requests/addasset.h b/API/requests/addasset.h index 3bbc117..82c955d 100644 --- a/API/requests/addasset.h +++ b/API/requests/addasset.h @@ -15,11 +15,11 @@ class AddAsset : public Post { public: AddAsset ( - const QString& title, - const QString& icon, - const QColor& color, - Models::Currency::ID currency, - const QUrl& baseUrl + const QString& title, + const QString& icon, + const QColor& color, + Models::Currency::ID currency, + const QUrl& baseUrl ); }; diff --git a/API/requests/updateasset.cpp b/API/requests/updateasset.cpp new file mode 100644 index 0000000..e402c5a --- /dev/null +++ b/API/requests/updateasset.cpp @@ -0,0 +1,19 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "updateasset.h" + +Request::UpdateAsset::UpdateAsset ( + Models::Asset::ID id, + const QString& title, + const QString& icon, + const QColor& color, + Models::Currency::ID currency, + const QUrl& baseUrl +): + Post(createUrl(baseUrl, "/updateAsset"), { + {"id", std::to_string(id).c_str()}, {"title", title}, {"icon", icon}, {"currency", std::to_string(currency).c_str()}, {"color", std::to_string(color.rgba()).c_str()} +}) +{ + emptyResult = true; +} diff --git a/API/requests/updateasset.h b/API/requests/updateasset.h new file mode 100644 index 0000000..44d5bd9 --- /dev/null +++ b/API/requests/updateasset.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "post.h" +#include "models/assets.h" +#include "models/currencies.h" + +namespace Request { + +class UpdateAsset : public Post { + Q_OBJECT + +public: + UpdateAsset( + Models::Asset::ID id, + const QString& title, + const QString& icon, + const QColor& color, + Models::Currency::ID currency, + const QUrl& baseUrl + ); +}; +} diff --git a/models/assets.cpp b/models/assets.cpp index 7ef2517..635eb02 100644 --- a/models/assets.cpp +++ b/models/assets.cpp @@ -6,7 +6,7 @@ #include "utils/helpers.h" const QHash Models::Assets::roles({ - {Title, "title"}, {Icon, "icon"}, {Balance, "balance"}, {Archived, "archived"}, {Color, "color"}, {Currency, "currency"}, {Id, "assetId"} + {Title, "title"}, {Icon, "icon"}, {Balance, "balance"}, {Archived, "archived"}, {Color, "color"}, {Currency, "currency"}, {CurrencyID, "currencyID"}, {ID, "assetID"} }); Models::Assets::Assets (Currencies& currencies, QObject* parent): @@ -23,8 +23,8 @@ void Models::Assets::clear () { } void Models::Assets::add (const Asset& asset) { - QModelIndex index = getIndex(asset.id); - if (index.isValid()) + int index = getIndexByID(asset.id); + if (index != -1) throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model"); beginInsertRows(QModelIndex(), records.size(), records.size()); @@ -37,7 +37,7 @@ void Models::Assets::add (const std::deque& assets) { return; for (const Asset& asset : assets) - if (getIndex(asset.id).isValid()) + if (getIndexByID(asset.id) != -1) 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); @@ -47,17 +47,24 @@ void Models::Assets::add (const std::deque& assets) { endInsertRows(); } -void Models::Assets::remove (unsigned int id) { - QModelIndex index = getIndex(id); - if (!index.isValid()) +void Models::Assets::remove (Asset::ID id) { + int index = getIndexByID(id); + if (index == -1) throw std::runtime_error("An attempt to delete non existing Asset from asset model"); - int row = index.row(); - beginRemoveRows(QModelIndex(), row, row); - records.erase(records.begin() + row); + beginRemoveRows(QModelIndex(), index, index); + records.erase(records.begin() + index); endRemoveRows(); } +Models::Asset Models::Assets::get (Asset::ID id) const { + int index = getIndexByID(id); + if (index == -1) + throw std::runtime_error("An attempt to access non existing Asset from asset model"); + + return records[index]; +} + 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 @@ -104,7 +111,9 @@ QVariant Models::Assets::data (const QModelIndex& index, int role) const { return records[row].color; case Currency: return currencies.getCode(records[row].currency); - case Id: + case CurrencyID: + return records[row].currency; + case ID: return records[row].id; } } @@ -139,11 +148,27 @@ void Models::Assets::receivedAssets (const std::deque& assets) { endResetModel(); } -QModelIndex Models::Assets::getIndex (unsigned int id) const { +int Models::Assets::getIndexByID (Asset::ID id) const { for (std::size_t i = 0; i < records.size(); ++i) { if (records[i].id == id) - return createIndex(i, 0, &records[i]); + return i; } - return QModelIndex(); + return -1; +} + +QVariantMap Models::Assets::getAssetByIndex (int index) const { + QVariantMap result; + if (index < 0 || index >= records.size()) + return result; + + QModelIndex idx = createIndex(index, 0, &records[index]); + for (int role = Roles::Title; role != Roles::ID; ++role) + result[roles[role]] = data(idx, role); + + return result; +} + +QVariantMap Models::Assets::getAssetByID (Asset::ID id) const { + return getAssetByIndex(getIndexByID(id)); } diff --git a/models/assets.h b/models/assets.h index b882914..5e2fe18 100644 --- a/models/assets.h +++ b/models/assets.h @@ -14,7 +14,9 @@ namespace Models { struct Asset { - unsigned int id; + using ID = uint32_t; + + ID id; QString title; QString icon; QColor color; @@ -37,13 +39,16 @@ public: Archived, Color, Currency, - Id + CurrencyID, + ID }; void clear (); void add (const Asset& asset); void add (const std::deque& assets); - void remove (unsigned int id); + void remove (Asset::ID id); + + Asset get(Asset::ID id) const; //Basic functionality: int rowCount (const QModelIndex& parent = QModelIndex()) const override; @@ -57,15 +62,16 @@ public: static bool deserialize (const QVariantList& from, std::deque& out); static const QHash roles; + Q_INVOKABLE int getIndexByID (Asset::ID id) const; + Q_INVOKABLE QVariantMap getAssetByIndex(int index) const; + Q_INVOKABLE QVariantMap getAssetByID(Asset::ID id) const; + signals: void requestAssets (); public slots: void receivedAssets (const std::deque& assets); -private: - QModelIndex getIndex (unsigned int id) const; - private: enum class State { initial, diff --git a/qml/Application/Assets.qml b/qml/Application/Assets.qml index 27b6367..fbfd17a 100644 --- a/qml/Application/Assets.qml +++ b/qml/Application/Assets.qml @@ -9,7 +9,11 @@ import magpie import magpie.Components as Components Item { - signal add + signal add() + signal edit(id: int) + signal remove(id: int) + + id: item ColumnLayout { id: column @@ -33,6 +37,9 @@ Item { delegate: Components.AssetLine { height: 30 width: listView.width + + onRemove: id => item.remove(id) + onEdit: id => item.edit(id) } } } diff --git a/qml/Application/Main.qml b/qml/Application/Main.qml index 85a30cd..c457263 100644 --- a/qml/Application/Main.qml +++ b/qml/Application/Main.qml @@ -6,7 +6,9 @@ import QtQuick.Controls import QtQuick.Layouts Item { - signal addAsset + signal addAsset() + signal removeAsset(id: int) + signal editAsset(id: int) property string currentPage: "home" RowLayout { @@ -71,6 +73,8 @@ Item { Assets { StackView.onActivating: currentPage = "assets" onAdd: addAsset() + onRemove: id => removeAsset(id) + onEdit: id => editAsset(id) } } diff --git a/qml/Application/Root.qml b/qml/Application/Root.qml index cd2cfb2..ad1f74a 100644 --- a/qml/Application/Root.qml +++ b/qml/Application/Root.qml @@ -19,6 +19,23 @@ Item { id: main Main { onAddAsset: stack.push(addAssetForm); + onEditAsset: function(id) { + const asset = Magpie.assets.getAssetByID(id); + stack.push(editAssetForm, { + name: asset.title, + icon: asset.icon, + color: asset.color, + currency: asset.currencyID, + assetID: asset.assetID, + title: qsTr("Editing asset") + " " + asset.title + }); + } + onRemoveAsset: function(id) { + API.deleteAsset(id, function(err) { + if (err) + Magpie.displayError("Error deleting asset " + Magpie.assets.getAssetByID(id).title + ": " + err); + }); + } } } @@ -53,6 +70,40 @@ Item { } } + Component { + id: editAssetForm + + Forms.Asset { + required property int assetID + + onCancel: stack.pop() + onConfirm: function (title, icon, color, currency) { + if (modal.inProgress) + return; + + modal.inProgress = true; + modal.status = qsTr("Updating asset ") + " " + Magpie.assets.getAssetByID(assetID).title + "..."; + modal.open(); + + API.updateAsset(assetID, title, icon, color, currency, function (err, result) { + if (!modal.inProgress) + return; + + modal.inProgress = false; + if (err) + modal.status = err; + else + modal.status = qsTr("Success"); + + if (!!result) { + modal.close(); + stack.pop() + } + }); + } + } + } + Connections { target: Magpie function onDisplayError (err) { diff --git a/qml/Components/AssetLine.qml b/qml/Components/AssetLine.qml index eed99fa..fec4948 100644 --- a/qml/Components/AssetLine.qml +++ b/qml/Components/AssetLine.qml @@ -13,13 +13,14 @@ Item { required property color color required property string balance required property string currency - required property int assetId + required property int assetID - signal error (err:string) + signal remove(id: int) + signal edit(id: int) Row { readonly property int iconSize: height - readonly property int freespace: width - deleteButton.width - iconSize - spacing * children.length - 1 + readonly property int freespace: width - deleteButton.width - editButton.width - iconSize - spacing * children.length - 1 anchors.fill: parent spacing: 5 @@ -37,29 +38,34 @@ Item { } } - Text { + Label { width: parent.freespace / 3 height: parent.height text: title verticalAlignment: Text.AlignVCenter - color: palette.text font.bold: true } - Text { + Label { width: parent.freespace / 3 height: parent.height text: balance verticalAlignment: Text.AlignVCenter - color: palette.text } - Text { + Label { width: parent.freespace / 3 height: parent.height text: currency verticalAlignment: Text.AlignVCenter - color: palette.text + } + + Button { + id: editButton + text: qsTr("Edit") + flat: true + height: parent.height + onClicked: edit(assetID) } Button { @@ -67,10 +73,7 @@ Item { text: qsTr("Delete") flat: true height: parent.height - onClicked: API.deleteAsset(line.assetId, function(err) { - if (err) - Magpie.displayError("Error deleting asset " + line.title + ": " + err); - }) + onClicked: remove(assetID) } } } diff --git a/qml/Components/ColorPicker.qml b/qml/Components/ColorPicker.qml index 95eb897..f00acd7 100644 --- a/qml/Components/ColorPicker.qml +++ b/qml/Components/ColorPicker.qml @@ -34,9 +34,17 @@ ComboBox { onActivated: index => box.color = model[index] Component.onCompleted: { - popup.background.color = box.background.color - popup.background.border.color = box.background.border.color - popup.background.border.width = box.background.border.width + if (box.background.color) + popup.background.color = box.background.color; + + if (!box.background.border) + return; + + if (box.background.border.color) + popup.background.border.color = box.background.border.color; + + if (box.background.border.width) + popup.background.border.width = box.background.border.width; } popup: Popup { @@ -69,8 +77,9 @@ ComboBox { highlighted: view.currentIndex === index contentItem: Rectangle { + anchors.fill: parent color: modelData - radius: box.background.radius + radius: box.background.radius || 0 } MouseArea { @@ -96,7 +105,7 @@ ComboBox { } background: Rectangle { - radius: box.background.radius + radius: box.background.radius || 0 } } } diff --git a/qml/Components/IconPicker.qml b/qml/Components/IconPicker.qml index 1524edd..2c14397 100644 --- a/qml/Components/IconPicker.qml +++ b/qml/Components/IconPicker.qml @@ -33,10 +33,11 @@ ComboBox { Icon { anchors.verticalCenter: parent.verticalCenter iconName: icon - color: palette.text + color: label.color } Label { + id: label anchors.verticalCenter: parent.verticalCenter text: icon } diff --git a/root.cpp b/root.cpp index 5b0cbfc..c80dd9f 100644 --- a/root.cpp +++ b/root.cpp @@ -1,11 +1,13 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "root.h" #include +#include +#include -Root::Root(const QUrl& root, int& argc, char* argv[]) : +Root::Root (const QUrl& root, int& argc, char* argv[]): QGuiApplication(argc, argv), root(root), engine(), @@ -13,7 +15,11 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) : magpie(), api(std::make_shared(magpie)) { - std::cout << "Starting Magpie..." << std::endl; + //QQuickStyle::setStyle("Basic"); + //QQuickStyle::setStyle("Material"); + //QQuickStyle::setStyle("Universal"); + //QQuickStyle::setStyle("Imagine"); + std::cout << "Starting Magpie in style " << QQuickStyle::name().toStdString() << std::endl; setOrganizationName("macaw.me"); setOrganizationDomain("macaw.me"); @@ -24,8 +30,11 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) : magpie.installAPI(api); - connect(&engine, &QQmlApplicationEngine::objectCreated, - this, &Root::onObjectCreated, + connect( + &engine, + &QQmlApplicationEngine::objectCreated, + this, + &Root::onObjectCreated, Qt::QueuedConnection ); @@ -49,11 +58,9 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) : throw std::runtime_error("Couldn't looad root qml object"); } -Root::~Root() { +Root::~Root () {} -} - -bool Root::notify(QObject* receiver, QEvent* e) { +bool Root::notify (QObject* receiver, QEvent* e) { try { return QGuiApplication::notify(receiver, e); } catch (const std::exception& e) { @@ -69,17 +76,17 @@ bool Root::notify(QObject* receiver, QEvent* e) { return false; } -void Root::onObjectCreated(QObject* obj, const QUrl& objUrl) { +void Root::onObjectCreated (QObject* obj, const QUrl& objUrl) { if (!obj && objUrl == root) exit(-2); } -void Root::onAPIAddressChanged(const QUrl& url) { +void Root::onAPIAddressChanged (const QUrl& url) { QSettings settings; settings.setValue("address", url); } -void Root::onStoreTokens(const QString& access, const QString& renew) { +void Root::onStoreTokens (const QString& access, const QString& renew) { QSettings settings; settings.setValue("accessToken", access); settings.setValue("renewToken", renew);