diff --git a/CMakeLists.txt b/CMakeLists.txt index cc4c303..773dc69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,8 @@ endif() # Source files set(SOURCES + src/AccountsModel.cpp + src/AccountsModel.h src/BlockchainPlugin.cpp src/BlockchainPlugin.h src/BlockchainBackend.cpp diff --git a/flake.lock b/flake.lock index 54b725c..4fb78e5 100644 --- a/flake.lock +++ b/flake.lock @@ -23,16 +23,16 @@ "rust-overlay": "rust-overlay" }, "locked": { - "lastModified": 1772023618, - "narHash": "sha256-rDjDhC9CxwPK2b0Pwc7UXh+xCy5EkK+T5LJ0I7j2OHM=", + "lastModified": 1772112497, + "narHash": "sha256-W0CjjkxOTHMBCn5MYpcXT0F4183Xd63obyyoSvYHyXg=", "owner": "logos-blockchain", "repo": "logos-blockchain", - "rev": "fed5efe6bd52c0d62d490c35f5c2a4368b94342f", + "rev": "2b1b24bf94e8cdbbb0dcf7bdf6f1769f9b421f85", "type": "github" }, "original": { "owner": "logos-blockchain", - "ref": "fed5efe6bd52c0d62d490c35f5c2a4368b94342f", + "ref": "0.1.9", "repo": "logos-blockchain", "type": "github" } @@ -68,11 +68,11 @@ ] }, "locked": { - "lastModified": 1772111643, - "narHash": "sha256-5avo4LCEpQrA076qTUXPJXPhwOZ0Ma96p04qT1vSve0=", + "lastModified": 1772125471, + "narHash": "sha256-gJxdNKbfJ+GqrjVXXQtXILfFi4RvRTNHvQthVu+7zzU=", "owner": "logos-blockchain", "repo": "logos-blockchain-module", - "rev": "6379fceb6dbd3c27c22468f412c446453733e96d", + "rev": "a1e6f7797adeb2c2737caf23a084732609a973e4", "type": "github" }, "original": { @@ -214,11 +214,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1770132997, - "narHash": "sha256-Iv0QMXMD6kf+y2Qx37jXR7Ik6h1dqOzuxBzCdc5S6KA=", + "lastModified": 1772028960, + "narHash": "sha256-BDWFjaKeoJW8oWDlPphNINt5U3P1xt1z1Y4f9jyC7uU=", "owner": "logos-co", "repo": "logos-cpp-sdk", - "rev": "30ef7986f4b65b7dcf43af84bb073233b1b77821", + "rev": "95f763b48d74bcdc63093b05159f43500cab139e", "type": "github" }, "original": { @@ -247,7 +247,7 @@ }, "logos-cpp-sdk_10": { "inputs": { - "nixpkgs": "nixpkgs_13" + "nixpkgs": "nixpkgs_15" }, "locked": { "lastModified": 1767724329, @@ -265,7 +265,7 @@ }, "logos-cpp-sdk_11": { "inputs": { - "nixpkgs": "nixpkgs_14" + "nixpkgs": "nixpkgs_16" }, "locked": { "lastModified": 1767724329, @@ -283,7 +283,7 @@ }, "logos-cpp-sdk_12": { "inputs": { - "nixpkgs": "nixpkgs_15" + "nixpkgs": "nixpkgs_17" }, "locked": { "lastModified": 1767724329, @@ -301,7 +301,7 @@ }, "logos-cpp-sdk_13": { "inputs": { - "nixpkgs": "nixpkgs_16" + "nixpkgs": "nixpkgs_18" }, "locked": { "lastModified": 1764699992, @@ -319,7 +319,7 @@ }, "logos-cpp-sdk_14": { "inputs": { - "nixpkgs": "nixpkgs_18" + "nixpkgs": "nixpkgs_20" }, "locked": { "lastModified": 1761230734, @@ -337,7 +337,7 @@ }, "logos-cpp-sdk_15": { "inputs": { - "nixpkgs": "nixpkgs_19" + "nixpkgs": "nixpkgs_21" }, "locked": { "lastModified": 1761230734, @@ -355,7 +355,7 @@ }, "logos-cpp-sdk_16": { "inputs": { - "nixpkgs": "nixpkgs_20" + "nixpkgs": "nixpkgs_22" }, "locked": { "lastModified": 1772028960, @@ -373,7 +373,7 @@ }, "logos-cpp-sdk_17": { "inputs": { - "nixpkgs": "nixpkgs_21" + "nixpkgs": "nixpkgs_23" }, "locked": { "lastModified": 1761230734, @@ -391,7 +391,7 @@ }, "logos-cpp-sdk_18": { "inputs": { - "nixpkgs": "nixpkgs_22" + "nixpkgs": "nixpkgs_24" }, "locked": { "lastModified": 1761230734, @@ -409,7 +409,7 @@ }, "logos-cpp-sdk_19": { "inputs": { - "nixpkgs": "nixpkgs_23" + "nixpkgs": "nixpkgs_25" }, "locked": { "lastModified": 1767724329, @@ -445,7 +445,7 @@ }, "logos-cpp-sdk_20": { "inputs": { - "nixpkgs": "nixpkgs_24" + "nixpkgs": "nixpkgs_26" }, "locked": { "lastModified": 1767724329, @@ -466,11 +466,11 @@ "nixpkgs": "nixpkgs_6" }, "locked": { - "lastModified": 1767724329, - "narHash": "sha256-UPkqxqxbKwU5Dmu00TnjiJVXUmfVylF3p1qziEuYwIE=", + "lastModified": 1772028960, + "narHash": "sha256-BDWFjaKeoJW8oWDlPphNINt5U3P1xt1z1Y4f9jyC7uU=", "owner": "logos-co", "repo": "logos-cpp-sdk", - "rev": "32f1d7080d784ff044d91d076ef2f0c7305d4784", + "rev": "95f763b48d74bcdc63093b05159f43500cab139e", "type": "github" }, "original": { @@ -499,7 +499,7 @@ }, "logos-cpp-sdk_5": { "inputs": { - "nixpkgs": "nixpkgs_8" + "nixpkgs": "nixpkgs_10" }, "locked": { "lastModified": 1761230734, @@ -517,7 +517,7 @@ }, "logos-cpp-sdk_6": { "inputs": { - "nixpkgs": "nixpkgs_9" + "nixpkgs": "nixpkgs_11" }, "locked": { "lastModified": 1761230734, @@ -535,7 +535,7 @@ }, "logos-cpp-sdk_7": { "inputs": { - "nixpkgs": "nixpkgs_10" + "nixpkgs": "nixpkgs_12" }, "locked": { "lastModified": 1770132997, @@ -553,7 +553,7 @@ }, "logos-cpp-sdk_8": { "inputs": { - "nixpkgs": "nixpkgs_11" + "nixpkgs": "nixpkgs_13" }, "locked": { "lastModified": 1761230734, @@ -571,7 +571,7 @@ }, "logos-cpp-sdk_9": { "inputs": { - "nixpkgs": "nixpkgs_12" + "nixpkgs": "nixpkgs_14" }, "locked": { "lastModified": 1761230734, @@ -612,6 +612,8 @@ "logos-capability-module": "logos-capability-module", "logos-cpp-sdk": "logos-cpp-sdk_3", "logos-module": "logos-module", + "nix-bundle-appimage": "nix-bundle-appimage", + "nix-bundle-dir": "nix-bundle-dir_2", "nixpkgs": [ "logos-blockchain-module", "logos-liblogos", @@ -620,11 +622,11 @@ ] }, "locked": { - "lastModified": 1771871578, - "narHash": "sha256-6Mu3cmdhd8e7i+n8OWcaIBye+i12gwlwt1fhd9QCbCI=", + "lastModified": 1772115748, + "narHash": "sha256-sPdAuYiLOjsulrk+uKMT7EG05ZlGT7OYEpgUh+f0nME=", "owner": "logos-co", "repo": "logos-liblogos", - "rev": "19d29d4ef99292d9285b3a561cb7ea8029be3b74", + "rev": "07780444deb99f10e600247e3696ba495f2f071a", "type": "github" }, "original": { @@ -852,11 +854,11 @@ ] }, "locked": { - "lastModified": 1770062426, - "narHash": "sha256-zc7ZxDTlqOCYGyEHhrTA/7GS1EWh7+4amdPUKh+gGds=", + "lastModified": 1770999556, + "narHash": "sha256-anpsEniGTTwUAwknRxjaT9GP4avHzIsolEHdHDTV9rM=", "owner": "logos-co", "repo": "logos-module", - "rev": "f7ee69d9ad9f27c84f04f59896e9194125e951dc", + "rev": "d1b35f335f938bb5de21a2a6010f1104075bdb1c", "type": "github" }, "original": { @@ -944,7 +946,7 @@ }, "logos-package": { "inputs": { - "nixpkgs": "nixpkgs_17" + "nixpkgs": "nixpkgs_19" }, "locked": { "lastModified": 1768925546, @@ -987,6 +989,66 @@ "type": "github" } }, + "nix-bundle-appimage": { + "inputs": { + "nix-bundle-dir": "nix-bundle-dir", + "nixpkgs": "nixpkgs_8" + }, + "locked": { + "lastModified": 1772047346, + "narHash": "sha256-RUsTUxKCxuQ3+D2LfBbK0EX1vF7HNMkpWgOGFfZbrEg=", + "owner": "logos-co", + "repo": "nix-bundle-appimage", + "rev": "4d68437c97ac59c3c70c1b2b116235c434d571a8", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "nix-bundle-appimage", + "type": "github" + } + }, + "nix-bundle-dir": { + "inputs": { + "nixpkgs": [ + "logos-blockchain-module", + "logos-liblogos", + "nix-bundle-appimage", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1771971384, + "narHash": "sha256-fq0H+sxQhkGN054jdN+ZfHZibbOjHA+KD5SpRH78T1g=", + "owner": "logos-co", + "repo": "nix-bundle-dir", + "rev": "1ecb9662145a1ad84007a970b4bef50a4af159c9", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "nix-bundle-dir", + "type": "github" + } + }, + "nix-bundle-dir_2": { + "inputs": { + "nixpkgs": "nixpkgs_9" + }, + "locked": { + "lastModified": 1771971384, + "narHash": "sha256-fq0H+sxQhkGN054jdN+ZfHZibbOjHA+KD5SpRH78T1g=", + "owner": "logos-co", + "repo": "nix-bundle-dir", + "rev": "1ecb9662145a1ad84007a970b4bef50a4af159c9", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "nix-bundle-dir", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1769461804, @@ -1117,11 +1179,11 @@ }, "nixpkgs_17": { "locked": { - "lastModified": 1768127708, - "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", "type": "github" }, "original": { @@ -1149,11 +1211,11 @@ }, "nixpkgs_19": { "locked": { - "lastModified": 1759036355, - "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "lastModified": 1768127708, + "narHash": "sha256-1Sm77VfZh3mU0F5OqKABNLWxOuDeHIlcFjsXeeiPazs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "rev": "ffbc9f8cbaacfb331b6017d5a5abb21a492c9a38", "type": "github" }, "original": { @@ -1259,6 +1321,38 @@ "type": "github" } }, + "nixpkgs_25": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_26": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "nixpkgs_3": { "locked": { "lastModified": 1759036355, @@ -1341,11 +1435,11 @@ }, "nixpkgs_8": { "locked": { - "lastModified": 1759036355, - "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "lastModified": 1771848320, + "narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", "type": "github" }, "original": { @@ -1357,11 +1451,11 @@ }, "nixpkgs_9": { "locked": { - "lastModified": 1759036355, - "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "lastModified": 1770562336, + "narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "rev": "d6c71932130818840fc8fe9509cf50be8c64634f", "type": "github" }, "original": { diff --git a/src/AccountsModel.cpp b/src/AccountsModel.cpp new file mode 100644 index 0000000..8bb03a8 --- /dev/null +++ b/src/AccountsModel.cpp @@ -0,0 +1,73 @@ +#include "AccountsModel.h" + +int AccountsModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return m_entries.size(); +} + +QVariant AccountsModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size()) + return QVariant(); + const Entry& e = m_entries.at(index.row()); + switch (role) { + case AddressRole: + return e.address; + case BalanceRole: + return e.balance; + case Qt::DisplayRole: + return e.address; + default: + return QVariant(); + } +} + +QHash AccountsModel::roleNames() const +{ + QHash names; + names[AddressRole] = "address"; + names[BalanceRole] = "balance"; + return names; +} + +void AccountsModel::setAddresses(const QStringList& addresses) +{ + QHash balanceCache; + for (const Entry& e : m_entries) + balanceCache.insert(e.address, e.balance); + + QVector newEntries; + newEntries.reserve(addresses.size()); + for (const QString& addr : addresses) { + Entry e; + e.address = addr; + e.balance = balanceCache.value(addr, QStringLiteral("---")); + newEntries.append(e); + } + + if (m_entries == newEntries) + return; + + beginResetModel(); + m_entries = std::move(newEntries); + endResetModel(); +} + +void AccountsModel::setBalanceForAddress(const QString& address, const QString& balance) +{ + const QString valueToSet = balance.trimmed().startsWith(QStringLiteral("Error")) + ? QStringLiteral("---") + : balance; + for (int i = 0; i < m_entries.size(); ++i) { + if (m_entries[i].address == address) { + if (m_entries[i].balance != valueToSet) { + m_entries[i].balance = valueToSet; + const QModelIndex idx = index(i, 0); + emit dataChanged(idx, idx, { BalanceRole }); + } + return; + } + } +} diff --git a/src/AccountsModel.h b/src/AccountsModel.h new file mode 100644 index 0000000..ccde43e --- /dev/null +++ b/src/AccountsModel.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include + +class AccountsModel : public QAbstractListModel { + Q_OBJECT +public: + enum Roles { AddressRole = Qt::UserRole + 1, BalanceRole }; + + explicit AccountsModel(QObject* parent = nullptr) : QAbstractListModel(parent) {} + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void setAddresses(const QStringList& addresses); + Q_INVOKABLE void setBalanceForAddress(const QString& address, const QString& balance); + +private: + struct Entry { + QString address; + QString balance; + bool operator==(const Entry& other) const { + return address == other.address && balance == other.balance; + } + }; + QVector m_entries; +}; diff --git a/src/BlockchainBackend.cpp b/src/BlockchainBackend.cpp index 09f5b83..fe7da1e 100644 --- a/src/BlockchainBackend.cpp +++ b/src/BlockchainBackend.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,7 @@ BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent) m_userConfig(""), m_deploymentConfig(""), m_logModel(new LogModel(this)), + m_accountsModel(new AccountsModel(this)), m_logosAPI(nullptr), m_blockchainClient(nullptr) { @@ -122,11 +124,15 @@ void BlockchainBackend::copyToClipboard(const QString& text) QString BlockchainBackend::getBalance(const QString& addressHex) { + QString result; if (!m_blockchainClient) { - return QStringLiteral("Error: Module not initialized."); + result = QStringLiteral("Error: Module not initialized."); + } else { + QVariant v = m_blockchainClient->invokeRemoteMethod(BLOCKCHAIN_MODULE_NAME, "wallet_get_balance", addressHex); + result = v.isValid() ? v.toString() : QStringLiteral("Error: Call failed."); } - QVariant result = m_blockchainClient->invokeRemoteMethod(BLOCKCHAIN_MODULE_NAME, "wallet_get_balance", addressHex); - return result.isValid() ? result.toString() : QStringLiteral("Error: Call failed."); + m_accountsModel->setBalanceForAddress(addressHex, result); + return result; } QString BlockchainBackend::transferFunds(const QString& fromKeyHex, const QString& toKeyHex, const QString& amountStr) @@ -160,7 +166,7 @@ void BlockchainBackend::startBlockchain() if (resultCode == 0 || resultCode == 1) { setStatus(Running); - QTimer::singleShot(500, this, [this]() { refreshKnownAddresses(); }); + QTimer::singleShot(500, this, [this]() { refreshAccounts(); }); } else if (resultCode == 2) { setStatus(ErrorConfigMissing); } else if (resultCode == 3) { @@ -170,15 +176,25 @@ void BlockchainBackend::startBlockchain() } } -void BlockchainBackend::refreshKnownAddresses() +void BlockchainBackend::refreshAccounts() { if (!m_blockchainClient) return; QVariant result = m_blockchainClient->invokeRemoteMethod(BLOCKCHAIN_MODULE_NAME, "wallet_get_known_addresses"); QStringList list = result.isValid() && result.canConvert() ? result.toStringList() : QStringList(); qDebug() << "BlockchainBackend: received from blockchain lib: type=QStringList, count=" << list.size(); - if (m_knownAddresses != list) { - m_knownAddresses = std::move(list); - emit knownAddressesChanged(); + m_accountsModel->setAddresses(list); + QTimer::singleShot(0, this, [this, list]() { fetchBalancesForAccounts(list); }); +} + +void BlockchainBackend::fetchBalancesForAccounts(const QStringList& list) +{ + if (!m_blockchainClient) return; + const int n = list.size(); + for (int i = 0; i < n; ++i) { + const QString address = list[i]; + if (address.isEmpty()) continue; + const QString balance = getBalance(address); + m_accountsModel->setBalanceForAddress(address, balance); } } diff --git a/src/BlockchainBackend.h b/src/BlockchainBackend.h index 3150e3f..9dad1f6 100644 --- a/src/BlockchainBackend.h +++ b/src/BlockchainBackend.h @@ -6,6 +6,7 @@ #include #include "logos_api.h" #include "logos_api_client.h" +#include "AccountsModel.h" #include "LogModel.h" class BlockchainBackend : public QObject { @@ -32,7 +33,7 @@ public: Q_PROPERTY(QString deploymentConfig READ deploymentConfig WRITE setDeploymentConfig NOTIFY deploymentConfigChanged) Q_PROPERTY(bool useGeneratedConfig READ useGeneratedConfig WRITE setUseGeneratedConfig NOTIFY useGeneratedConfigChanged) Q_PROPERTY(LogModel* logModel READ logModel CONSTANT) - Q_PROPERTY(QStringList knownAddresses READ knownAddresses NOTIFY knownAddressesChanged) + Q_PROPERTY(AccountsModel* accountsModel READ accountsModel CONSTANT) Q_PROPERTY(QString generatedUserConfigPath READ generatedUserConfigPath CONSTANT) explicit BlockchainBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr); @@ -43,7 +44,7 @@ public: QString deploymentConfig() const { return m_deploymentConfig; } bool useGeneratedConfig() const { return m_useGeneratedConfig; } LogModel* logModel() const { return m_logModel; } - QStringList knownAddresses() const { return m_knownAddresses; } + AccountsModel* accountsModel() const { return m_accountsModel; } void setUserConfig(const QString& path); void setDeploymentConfig(const QString& path); @@ -57,7 +58,7 @@ public: const QString& amountStr); Q_INVOKABLE void startBlockchain(); Q_INVOKABLE void stopBlockchain(); - Q_INVOKABLE void refreshKnownAddresses(); + Q_INVOKABLE void refreshAccounts(); Q_INVOKABLE int generateConfig(const QString& outputPath, const QStringList& initialPeers, int netPort, @@ -78,17 +79,17 @@ signals: void userConfigChanged(); void deploymentConfigChanged(); void useGeneratedConfigChanged(); - void knownAddressesChanged(); private: void setStatus(BlockchainStatus newStatus); + void fetchBalancesForAccounts(const QStringList& list); BlockchainStatus m_status; QString m_userConfig; QString m_deploymentConfig; bool m_useGeneratedConfig = false; LogModel* m_logModel; - QStringList m_knownAddresses; + AccountsModel* m_accountsModel; LogosAPI* m_logosAPI; LogosAPIClient* m_blockchainClient; diff --git a/src/blockchain_resources.qrc b/src/blockchain_resources.qrc index 38432f5..8fb2392 100644 --- a/src/blockchain_resources.qrc +++ b/src/blockchain_resources.qrc @@ -8,6 +8,11 @@ qml/views/GenerateConfigView.qml qml/views/ConfigChoiceView.qml qml/views/SetConfigPathView.qml + qml/controls/AccountDelegate.qml + qml/controls/LogosCopyButton.qml icons/blockchain.png + icons/copy.svg + icons/checkmark.svg + icons/refresh.svg diff --git a/src/icons/checkmark.svg b/src/icons/checkmark.svg new file mode 100644 index 0000000..e98ef59 --- /dev/null +++ b/src/icons/checkmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/copy.svg b/src/icons/copy.svg new file mode 100644 index 0000000..e695933 --- /dev/null +++ b/src/icons/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/refresh.svg b/src/icons/refresh.svg new file mode 100644 index 0000000..0336dd1 --- /dev/null +++ b/src/icons/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/BlockchainView.qml b/src/qml/BlockchainView.qml index 6836a71..ef1200a 100644 --- a/src/qml/BlockchainView.qml +++ b/src/qml/BlockchainView.qml @@ -90,12 +90,13 @@ Rectangle { SplitView { orientation: Qt.Vertical - RowLayout { + ColumnLayout { SplitView.fillWidth: true SplitView.minimumHeight: 200 + spacing: Theme.spacing.large StatusConfigView { - Layout.preferredWidth: parent.width / 2 + Layout.fillWidth: true statusText: _d.getStatusString(backend.status) statusColor: _d.getStatusColor(backend.status) userConfig: backend.userConfig @@ -113,16 +114,28 @@ Rectangle { WalletView { id: walletView - Layout.preferredWidth: parent.width / 2 - knownAddresses: backend.knownAddresses + accountsModel: backend.accountsModel onGetBalanceRequested: function(addressHex) { - walletView.setBalanceResult(backend.getBalance(addressHex)) + var result = backend.getBalance(addressHex) + if ((result || "").indexOf("Error") === 0) { + lastBalanceErrorAddress = addressHex + lastBalanceError = result + } + else { + lastBalanceErrorAddress = "" + lastBalanceError = "" + } } + onCopyToClipboard: (text) => backend.copyToClipboard(text) onTransferRequested: function(fromKeyHex, toKeyHex, amount) { walletView.setTransferResult(backend.transferFunds(fromKeyHex, toKeyHex, amount)) } } + + Item { + Layout.preferredHeight: Theme.spacing.small + } } LogsView { diff --git a/src/qml/controls/AccountDelegate.qml b/src/qml/controls/AccountDelegate.qml new file mode 100644 index 0000000..f583b5d --- /dev/null +++ b/src/qml/controls/AccountDelegate.qml @@ -0,0 +1,76 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Logos.Theme +import Logos.Controls + +ItemDelegate { + id: root + + property string balanceError: "" + + signal getBalanceRequested(string addressHex) + signal copyRequested(string text) + + width: ListView.view ? ListView.view.width : implicitWidth + + background: Rectangle { + color: root.hovered ? Theme.palette.backgroundSecondary : "transparent" + } + + contentItem: ColumnLayout { + spacing: Theme.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + + LogosText { + Layout.fillWidth: true + text: model.address || "" + elide: Text.ElideMiddle + font.pixelSize: Theme.typography.secondaryText + } + + LogosText { + Layout.preferredWidth: contentWidth + Layout.alignment: Qt.AlignRight + visible: (model.balance || "").length > 0 + text: model.balance || "" + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + elide: Text.ElideRight + } + + Button { + Layout.alignment: Qt.AlignRight + Layout.leftMargin: parent.spacing + Layout.preferredHeight: 40 + Layout.preferredWidth: 40 + display: AbstractButton.IconOnly + flat: true + icon.source: "qrc:/icons/refresh.svg" + font.pixelSize: Theme.typography.secondaryText + padding: 4 + onClicked: root.getBalanceRequested(model.address || "") + } + + LogosCopyButton { + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 40 + Layout.preferredWidth: 40 + onCopyText: root.copyRequested(model.address || "") + } + } + + LogosText { + Layout.fillWidth: true + visible: !!text + text: root.balanceError || "" + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.error + wrapMode: Text.WordWrap + } + } +} diff --git a/src/qml/controls/LogosCopyButton.qml b/src/qml/controls/LogosCopyButton.qml new file mode 100644 index 0000000..034c5dc --- /dev/null +++ b/src/qml/controls/LogosCopyButton.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Controls + +Button { + id: root + + signal copyText() + + implicitWidth: 24 + implicitHeight: 24 + display: AbstractButton.IconOnly + flat: true + + property string iconSource: "qrc:/icons/copy.svg" + + icon.source: root.iconSource + icon.width: 24 + icon.height: 24 + + function reset() { + iconSource = "qrc:/icons/copy.svg" + } + + Timer { + id: resetTimer + interval: 1500 + repeat: false + onTriggered: root.reset() + } + + onClicked: { + root.copyText() + root.iconSource = "qrc:/icons/checkmark.svg" + resetTimer.restart() + } +} diff --git a/src/qml/views/GenerateConfigView.qml b/src/qml/views/GenerateConfigView.qml index cc14246..41e47aa 100644 --- a/src/qml/views/GenerateConfigView.qml +++ b/src/qml/views/GenerateConfigView.qml @@ -94,6 +94,7 @@ ColumnLayout { placeholderText: qsTr("Peer addresses, one per line") placeholderTextColor: Theme.palette.textTertiary font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.text } } diff --git a/src/qml/views/StatusConfigView.qml b/src/qml/views/StatusConfigView.qml index 926797e..b8584fa 100644 --- a/src/qml/views/StatusConfigView.qml +++ b/src/qml/views/StatusConfigView.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import Logos.Theme import Logos.Controls -ColumnLayout { +Rectangle { id: root // --- Public API --- @@ -21,124 +21,108 @@ ColumnLayout { signal stopRequested() signal changeConfigRequested() - spacing: Theme.spacing.large + implicitHeight: contentLayout.height + Theme.spacing.large + color: Theme.palette.backgroundTertiary + radius: Theme.spacing.radiusLarge + border.color: Theme.palette.border + border.width: 1 - // Status Card - Rectangle { - Layout.alignment: Qt.AlignTop - Layout.preferredWidth: parent.width * 0.9 - Layout.preferredHeight: implicitHeight - implicitHeight: statusContent.implicitHeight + 2 * Theme.spacing.large - color: Theme.palette.backgroundTertiary - radius: Theme.spacing.radiusLarge - border.color: Theme.palette.border - border.width: 1 + RowLayout { + id: contentLayout - ColumnLayout { - id: statusContent + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Theme.spacing.large + spacing: Theme.spacing.large - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Theme.spacing.large + // Status Card + RowLayout { + Layout.alignment: Qt.AlignVCenter spacing: Theme.spacing.medium - LogosText { - Layout.alignment: Qt.AlignLeft - font.bold: true - text: root.statusText - color: root.statusColor + ColumnLayout { + LogosText { + font.bold: true + text: root.statusText + color: root.statusColor + } + LogosText { + text: qsTr("Mainnet - chain ID 1") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } } - - LogosText { - Layout.alignment: Qt.AlignLeft - Layout.topMargin: -Theme.spacing.medium - text: qsTr("Mainnet - chain ID 1") - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - } - LogosButton { - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: parent.width - Layout.preferredHeight: 50 + Layout.preferredHeight: 40 + Layout.preferredWidth: 100 enabled: root.canStart text: root.isRunning ? qsTr("Stop Node") : qsTr("Start Node") onClicked: root.isRunning ? root.stopRequested() : root.startRequested() } } - } - // Config Card - Rectangle { - Layout.preferredWidth: parent.width * 0.9 - Layout.preferredHeight: implicitHeight - implicitHeight: contentLayout.implicitHeight + 2 * Theme.spacing.large + Rectangle { + Layout.preferredWidth: 1 + Layout.fillHeight: true + color: Theme.palette.borderSecondary + } - color: Theme.palette.backgroundTertiary - radius: Theme.spacing.radiusLarge - border.color: Theme.palette.border - border.width: 1 - - ColumnLayout { - id: contentLayout - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: Theme.spacing.large + // Config Card + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter spacing: Theme.spacing.medium - LogosText { - text: qsTr("Config") - font.bold: true - } - - RowLayout { + ColumnLayout { Layout.fillWidth: true - spacing: Theme.spacing.small - LogosText { - text: qsTr("User Config: ") - font.bold: true - } - LogosText { - Layout.fillWidth: true - text: (root.userConfig || qsTr("No file selected")) + - (root.useGeneratedConfig ? " " + qsTr("(Generated)") : "") - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - wrapMode: Text.WordWrap - } - } - RowLayout { - Layout.fillWidth: true - Layout.topMargin: -Theme.spacing.small - spacing: Theme.spacing.small - LogosText { - text: qsTr("Deployment Config: ") - font.bold: true - } - LogosText { + RowLayout { Layout.fillWidth: true - text: (root.useGeneratedConfig && root.deploymentConfig ? root.deploymentConfig : - root.useGeneratedConfig ? qsTr("Devnet (default)") : - (root.deploymentConfig || qsTr("No file selected"))) - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.textSecondary - wrapMode: Text.WordWrap + spacing: Theme.spacing.small + LogosText { + text: qsTr("User Config: ") + font.pixelSize: Theme.typography.secondaryText + } + LogosText { + Layout.fillWidth: true + text: (root.userConfig || qsTr("No file selected")) + + (root.useGeneratedConfig ? " " + qsTr("(Generated)") : "") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + wrapMode: Text.WordWrap + } + } + + RowLayout { + Layout.fillWidth: true + Layout.topMargin: -Theme.spacing.small + spacing: Theme.spacing.small + LogosText { + text: qsTr("Deployment Config: ") + font.pixelSize: Theme.typography.secondaryText + } + LogosText { + Layout.fillWidth: true + text: (root.useGeneratedConfig && root.deploymentConfig ? + root.deploymentConfig : + root.useGeneratedConfig ? + qsTr("Devnet (default)") : + (root.deploymentConfig || qsTr("No file selected"))) + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + wrapMode: Text.WordWrap + } } } LogosButton { Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - Layout.preferredHeight: 50 + Layout.preferredWidth: 100 + Layout.preferredHeight: 40 text: qsTr("Change") onClicked: root.changeConfigRequested() } } } - - Item { Layout.fillHeight: true } } diff --git a/src/qml/views/WalletView.qml b/src/qml/views/WalletView.qml index 9f857e9..a722a46 100644 --- a/src/qml/views/WalletView.qml +++ b/src/qml/views/WalletView.qml @@ -5,20 +5,20 @@ import QtQuick.Layouts import Logos.Theme import Logos.Controls -ColumnLayout { +import "../controls" + +RowLayout { id: root - // list of known wallet addresses for Get balance dropdown - property var knownAddresses: [] + required property var accountsModel + + property string lastBalanceError: "" + property string lastBalanceErrorAddress: "" - // --- Public API --- signal getBalanceRequested(string addressHex) signal transferRequested(string fromKeyHex, string toKeyHex, string amount) + signal copyToClipboard(string text) - // Call these from the parent to display results - function setBalanceResult(text) { - balanceResultText.text = text - } function setTransferResult(text) { transferResultText.text = text } @@ -28,8 +28,8 @@ ColumnLayout { // Get balance card Rectangle { Layout.fillWidth: true - implicitHeight: balanceCol.implicitHeight + 2 * Theme.spacing.large - Layout.preferredHeight: implicitHeight + implicitHeight: transferRect.height + Layout.preferredHeight: Math.min(implicitHeight, 400) color: Theme.palette.backgroundTertiary radius: Theme.spacing.radiusLarge border.color: Theme.palette.border @@ -44,40 +44,32 @@ ColumnLayout { spacing: Theme.spacing.large LogosText { - text: qsTr("Get balance") + text: qsTr("Accounts") font.pixelSize: Theme.typography.secondaryText font.bold: true } - // Dropdown of known addresses, or type a custom address - StyledAddressComboBox { - id: balanceAddressCombo - model: knownAddresses + LogosText { + text: qsTr("Start node to see accounts here.") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + wrapMode: Text.WordWrap + visible: balanceListView.count === 0 } - RowLayout { + ListView { + id: balanceListView Layout.fillWidth: true - Layout.preferredHeight: balanceButton.implicitHeight - spacing: Theme.spacing.large + Layout.preferredHeight: Math.min(contentHeight, 320) + clip: true + model: root.accountsModel + spacing: Theme.spacing.small - LogosButton { - id: balanceButton - text: qsTr("Get balance") - onClicked: root.getBalanceRequested(balanceAddressCombo.currentText.trim()) - } - - LogosButton { - Layout.fillWidth: true - enabled: false - padding: Theme.spacing.medium - contentItem: Text { - id: balanceResultText - width: parent.width - color: Theme.palette.textSecondary - font.pixelSize: Theme.typography.secondaryText - font.weight: Theme.typography.weightMedium - wrapMode: Text.WordWrap - } + delegate: AccountDelegate { + balanceError: root.lastBalanceErrorAddress === model.address ? + root.lastBalanceError: "" + onGetBalanceRequested: (addr) => root.getBalanceRequested(addr) + onCopyRequested: (text) => root.copyToClipboard(text) } } } @@ -85,6 +77,7 @@ ColumnLayout { // Transfer funds card Rectangle { + id: transferRect Layout.fillWidth: true Layout.preferredHeight: transferCol.height + 2 * Theme.spacing.large color: Theme.palette.backgroundTertiary @@ -108,7 +101,8 @@ ColumnLayout { StyledAddressComboBox { id: transferFromCombo - model: knownAddresses + model: root.accountsModel + textRole: "address" } LogosTextField { @@ -128,37 +122,44 @@ ColumnLayout { RowLayout { Layout.fillWidth: true Layout.preferredHeight: transferButton.implicitHeight - spacing: Theme.spacing.large LogosButton { id: transferButton - text: qsTr("Transfer") + Layout.preferredWidth: 60 Layout.alignment: Qt.AlignRight + text: qsTr("Send") onClicked: root.transferRequested(transferFromCombo.currentText.trim(), transferToField.text.trim(), transferAmountField.text) } LogosButton { Layout.fillWidth: true - enabled: false - padding: Theme.spacing.medium - contentItem: Text { - id: transferResultText + enabled: true + padding: Theme.spacing.small + contentItem: RowLayout { width: parent.width - color: Theme.palette.textSecondary - font.pixelSize: Theme.typography.secondaryText - font.weight: Theme.typography.weightMedium - wrapMode: Text.WordWrap + anchors.centerIn: parent + LogosText { + id: transferResultText + Layout.fillWidth: true + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + font.weight: Theme.typography.weightMedium + wrapMode: Text.WordWrap + elide: Text.ElideRight + } + LogosCopyButton { + Layout.alignment: Qt.AlignRight + Layout.preferredHeight: 40 + Layout.preferredWidth: 40 + onCopyText: root.copyRequested(transferResultText.text) + visible: transferResultText.text + } } } } } } - Item { - Layout.fillWidth: true - Layout.preferredHeight: Theme.spacing.small - } - component StyledAddressComboBox: ComboBox { id: comboControl @@ -195,7 +196,6 @@ ColumnLayout { bottomPadding: 0 verticalAlignment: Text.AlignVCenter font.pixelSize: Theme.typography.secondaryText - font.bold: true text: comboControl.editText onTextChanged: if (text !== comboControl.editText) comboControl.editText = text selectByMouse: true @@ -219,7 +219,7 @@ ColumnLayout { height: contentHeight + Theme.spacing.large font.pixelSize: Theme.typography.secondaryText font.bold: true - text: modelData + text: (typeof model.address !== "undefined" ? model.address : modelData) || "" elide: Text.ElideMiddle horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter