mirror of
https://github.com/logos-blockchain/logos-blockchain-ui.git
synced 2026-07-23 16:13:22 +00:00
feat: Revamp interface (#24)
* Modify views * Use structure view for blocks events * Stability fixes * Add cryptarchia info block * Hide settings button on run * Pinnable accounts operation * Use stylish pin button * Show balance on transfer * Stop node on leave * Add info box with peer id * Add vouchers view to leader claim with pending counter * Add help buttons * Remove tx useless label * Update module
This commit is contained in:
parent
40dd478ed4
commit
85b0b153f2
@ -18,8 +18,8 @@ logos_module(
|
||||
src/BlockchainBackend.cpp
|
||||
src/AccountsModel.h
|
||||
src/AccountsModel.cpp
|
||||
src/LogModel.h
|
||||
src/LogModel.cpp
|
||||
src/BlockModel.h
|
||||
src/BlockModel.cpp
|
||||
FIND_PACKAGES
|
||||
Qt6Gui
|
||||
LINK_LIBRARIES
|
||||
|
||||
16
flake.lock
generated
16
flake.lock
generated
@ -21,17 +21,17 @@
|
||||
"logos-module-builder": "logos-module-builder"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781796694,
|
||||
"narHash": "sha256-cW/sytFz22oMABjmTH6UHj3FS7Mo9kv62rbTir6s7rk=",
|
||||
"lastModified": 1782217354,
|
||||
"narHash": "sha256-pUt/GLJRhqj4Ap7WzGJtOVqlil1FF5s2NoCxPuAgt18=",
|
||||
"owner": "logos-blockchain",
|
||||
"repo": "logos-blockchain-module",
|
||||
"rev": "ab733aa7074cf1992f605c203c3d6a7923602705",
|
||||
"rev": "4c8df124929c6e86d21e2f0db50a99dedee901b3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "logos-blockchain",
|
||||
"repo": "logos-blockchain-module",
|
||||
"rev": "ab733aa7074cf1992f605c203c3d6a7923602705",
|
||||
"rev": "4c8df124929c6e86d21e2f0db50a99dedee901b3",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
@ -44,16 +44,16 @@
|
||||
"rust-rapidsnark": "rust-rapidsnark"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781698521,
|
||||
"narHash": "sha256-cnk7CmRskPL5CjH3Q0vZEoLbYVkwUaQrq86xdJiLje8=",
|
||||
"lastModified": 1782212034,
|
||||
"narHash": "sha256-IcU+c3YokqSmv4huTHZsBUhsen8lLs3wGvtaMWfQths=",
|
||||
"owner": "logos-blockchain",
|
||||
"repo": "logos-blockchain",
|
||||
"rev": "0222706b14010fbdfbe9a94d3617ebf46a77fdd1",
|
||||
"rev": "113faae6fd5f68aa70e3cece4fa54f8522c95eec",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "logos-blockchain",
|
||||
"ref": "0222706b14010fbdfbe9a94d3617ebf46a77fdd1",
|
||||
"ref": "113faae6fd5f68aa70e3cece4fa54f8522c95eec",
|
||||
"repo": "logos-blockchain",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
inputs = {
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder/38ddf92c1f240f4e420d300a1fbabb1609d5db01";
|
||||
nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx";
|
||||
liblogos_blockchain_module.url = "github:logos-blockchain/logos-blockchain-module/ab733aa7074cf1992f605c203c3d6a7923602705";
|
||||
liblogos_blockchain_module.url = "github:logos-blockchain/logos-blockchain-module/4c8df124929c6e86d21e2f0db50a99dedee901b3";
|
||||
};
|
||||
|
||||
outputs = inputs@{ logos-module-builder, ... }:
|
||||
|
||||
167
src/BlockModel.cpp
Normal file
167
src/BlockModel.cpp
Normal file
@ -0,0 +1,167 @@
|
||||
#include "BlockModel.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonValue>
|
||||
|
||||
namespace {
|
||||
|
||||
QString prettify(const QJsonValue& value)
|
||||
{
|
||||
if (value.isObject())
|
||||
return QString::fromUtf8(QJsonDocument(value.toObject()).toJson(QJsonDocument::Indented));
|
||||
if (value.isArray())
|
||||
return QString::fromUtf8(QJsonDocument(value.toArray()).toJson(QJsonDocument::Indented));
|
||||
return value.toVariant().toString();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int BlockModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return m_entries.size();
|
||||
}
|
||||
|
||||
QVariant BlockModel::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 TimestampRole: return e.timestamp;
|
||||
case SlotRole: return e.slot;
|
||||
case VersionRole: return e.version;
|
||||
case ParentBlockRole: return e.parentBlock;
|
||||
case BlockRootRole: return e.blockRoot;
|
||||
case LeaderKeyRole: return e.leaderKey;
|
||||
case EntropyRole: return e.entropy;
|
||||
case ProofRole: return e.proof;
|
||||
case VoucherCmRole: return e.voucherCm;
|
||||
case SignatureRole: return e.signature;
|
||||
case TxCountRole: return e.txCount;
|
||||
case TransactionsRole: return e.transactions;
|
||||
case RawJsonRole: return e.rawJson;
|
||||
case ParsedRole: return e.parsed;
|
||||
default: return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> BlockModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> names;
|
||||
names[TimestampRole] = "timestamp";
|
||||
names[SlotRole] = "slot";
|
||||
names[VersionRole] = "version";
|
||||
names[ParentBlockRole] = "parentBlock";
|
||||
names[BlockRootRole] = "blockRoot";
|
||||
names[LeaderKeyRole] = "leaderKey";
|
||||
names[EntropyRole] = "entropy";
|
||||
names[ProofRole] = "proof";
|
||||
names[VoucherCmRole] = "voucherCm";
|
||||
names[SignatureRole] = "signature";
|
||||
names[TxCountRole] = "txCount";
|
||||
names[TransactionsRole] = "transactions";
|
||||
names[RawJsonRole] = "rawJson";
|
||||
names[ParsedRole] = "parsed";
|
||||
return names;
|
||||
}
|
||||
|
||||
void BlockModel::appendRaw(const QString& timestamp, const QString& rawJson)
|
||||
{
|
||||
Entry e;
|
||||
e.timestamp = timestamp;
|
||||
|
||||
// The payload is double-encoded JSON: an outer {"block":"<json string>"}
|
||||
// whose value is itself a JSON-encoded block. Tolerate a few shapes:
|
||||
// { "block": "<stringified block>" } (observed)
|
||||
// { "block": { ... } } (already an object)
|
||||
// { "header": ..., "transactions": ... }(block sent directly)
|
||||
QJsonObject block;
|
||||
bool ok = false;
|
||||
|
||||
QJsonParseError err{};
|
||||
const QJsonDocument outer = QJsonDocument::fromJson(rawJson.toUtf8(), &err);
|
||||
if (err.error == QJsonParseError::NoError && outer.isObject()) {
|
||||
const QJsonObject o = outer.object();
|
||||
if (o.contains(QStringLiteral("block"))) {
|
||||
const QJsonValue bv = o.value(QStringLiteral("block"));
|
||||
if (bv.isString()) {
|
||||
const QJsonDocument inner =
|
||||
QJsonDocument::fromJson(bv.toString().toUtf8(), &err);
|
||||
if (err.error == QJsonParseError::NoError && inner.isObject()) {
|
||||
block = inner.object();
|
||||
ok = true;
|
||||
}
|
||||
} else if (bv.isObject()) {
|
||||
block = bv.toObject();
|
||||
ok = true;
|
||||
}
|
||||
} else if (o.contains(QStringLiteral("header"))) {
|
||||
block = o;
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
e.parsed = true;
|
||||
|
||||
const QJsonObject header = block.value(QStringLiteral("header")).toObject();
|
||||
e.version = header.value(QStringLiteral("version")).toString();
|
||||
e.parentBlock = header.value(QStringLiteral("parent_block")).toString();
|
||||
|
||||
const QJsonValue slotV = header.value(QStringLiteral("slot"));
|
||||
e.slot = slotV.isDouble()
|
||||
? QString::number(static_cast<qlonglong>(slotV.toDouble()))
|
||||
: slotV.toString();
|
||||
|
||||
e.blockRoot = header.value(QStringLiteral("block_root")).toString();
|
||||
|
||||
const QJsonObject pol =
|
||||
header.value(QStringLiteral("proof_of_leadership")).toObject();
|
||||
e.proof = pol.value(QStringLiteral("proof")).toString();
|
||||
e.entropy = pol.value(QStringLiteral("entropy_contribution")).toString();
|
||||
e.leaderKey = pol.value(QStringLiteral("leader_key")).toString();
|
||||
e.voucherCm = pol.value(QStringLiteral("voucher_cm")).toString();
|
||||
|
||||
e.signature = block.value(QStringLiteral("signature")).toString();
|
||||
|
||||
const QJsonArray txs = block.value(QStringLiteral("transactions")).toArray();
|
||||
e.txCount = txs.size();
|
||||
for (const QJsonValue tx : txs)
|
||||
e.transactions << prettify(tx);
|
||||
|
||||
e.rawJson = QString::fromUtf8(QJsonDocument(block).toJson(QJsonDocument::Indented));
|
||||
} else {
|
||||
// Keep the raw text so an unexpected format is still inspectable.
|
||||
e.parsed = false;
|
||||
e.rawJson = rawJson;
|
||||
}
|
||||
|
||||
beginInsertRows(QModelIndex(), 0, 0);
|
||||
m_entries.prepend(e);
|
||||
endInsertRows();
|
||||
|
||||
if (m_entries.size() > kMaxBlocks) {
|
||||
const int last = m_entries.size() - 1;
|
||||
beginRemoveRows(QModelIndex(), last, last);
|
||||
m_entries.remove(last);
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void BlockModel::clear()
|
||||
{
|
||||
if (m_entries.isEmpty())
|
||||
return;
|
||||
beginResetModel();
|
||||
m_entries.clear();
|
||||
endResetModel();
|
||||
emit countChanged();
|
||||
}
|
||||
69
src/BlockModel.h
Normal file
69
src/BlockModel.h
Normal file
@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
// Structured model of recent blockchain blocks, fed by the backend's `newBlock`
|
||||
// event. Each incoming payload is parsed once (in appendRaw) into header fields
|
||||
// plus a list of prettified per-transaction JSON; unparsable payloads are kept
|
||||
// as raw text (ParsedRole == false) so nothing is ever silently dropped.
|
||||
//
|
||||
// Newest block is row 0. Only the latest kMaxBlocks are retained.
|
||||
class BlockModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||
public:
|
||||
enum Roles {
|
||||
TimestampRole = Qt::UserRole + 1,
|
||||
SlotRole,
|
||||
VersionRole,
|
||||
ParentBlockRole,
|
||||
BlockRootRole,
|
||||
LeaderKeyRole,
|
||||
EntropyRole,
|
||||
ProofRole,
|
||||
VoucherCmRole,
|
||||
SignatureRole,
|
||||
TxCountRole,
|
||||
TransactionsRole, // QStringList: prettified JSON per transaction
|
||||
RawJsonRole, // prettified full block (or the raw payload on parse failure)
|
||||
ParsedRole, // bool: false when the payload could not be parsed
|
||||
};
|
||||
|
||||
explicit BlockModel(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<int, QByteArray> roleNames() const override;
|
||||
|
||||
// Parse a raw `newBlock` payload and insert it as the newest block (row 0),
|
||||
// evicting the oldest once kMaxBlocks is exceeded.
|
||||
Q_INVOKABLE void appendRaw(const QString& timestamp, const QString& rawJson);
|
||||
Q_INVOKABLE void clear();
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
QString timestamp;
|
||||
QString slot;
|
||||
QString version;
|
||||
QString parentBlock;
|
||||
QString blockRoot;
|
||||
QString leaderKey;
|
||||
QString entropy;
|
||||
QString proof;
|
||||
QString voucherCm;
|
||||
QString signature;
|
||||
int txCount = 0;
|
||||
QStringList transactions;
|
||||
QString rawJson;
|
||||
bool parsed = false;
|
||||
};
|
||||
|
||||
static constexpr int kMaxBlocks = 100;
|
||||
QVector<Entry> m_entries;
|
||||
};
|
||||
@ -119,7 +119,7 @@ BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
: BlockchainBackendSimpleSource(parent)
|
||||
, m_logosAPI(logosAPI)
|
||||
, m_accountsModel(new AccountsModel(this))
|
||||
, m_logModel(new LogModel(this))
|
||||
, m_blockModel(new BlockModel(this))
|
||||
{
|
||||
setStatus(NotStarted);
|
||||
setUseGeneratedConfig(false);
|
||||
@ -185,13 +185,8 @@ BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
[this](const QString&, const QVariantList& data) {
|
||||
const QString timestamp =
|
||||
QDateTime::currentDateTime().toString("HH:mm:ss");
|
||||
QString line;
|
||||
if (!data.isEmpty())
|
||||
line = QString("[%1] New block: %2")
|
||||
.arg(timestamp, data.first().toString());
|
||||
else
|
||||
line = QString("[%1] New block (no data)").arg(timestamp);
|
||||
m_logModel->append(line);
|
||||
const QString raw = data.isEmpty() ? QString() : data.first().toString();
|
||||
m_blockModel->appendRaw(timestamp, raw);
|
||||
});
|
||||
} else {
|
||||
setError(QStringLiteral("Failed to subscribe to events"));
|
||||
@ -215,6 +210,35 @@ QVariantMap BlockchainBackend::claimLeaderRewards()
|
||||
BLOCKCHAIN_MODULE_NAME, "leader_claim")));
|
||||
}
|
||||
|
||||
QVariantMap BlockchainBackend::getCryptarchiaInfo()
|
||||
{
|
||||
if (!m_blockchainClient)
|
||||
return result::toVariantMap(result::err(QStringLiteral("Module not initialized.")));
|
||||
|
||||
return result::toVariantMap(result::toLogosResult(m_blockchainClient->invokeRemoteMethod(
|
||||
BLOCKCHAIN_MODULE_NAME, QStringLiteral("get_cryptarchia_info"))));
|
||||
}
|
||||
|
||||
QVariantMap BlockchainBackend::getPeerId()
|
||||
{
|
||||
if (!m_blockchainClient)
|
||||
return result::toVariantMap(result::err(QStringLiteral("Module not initialized.")));
|
||||
|
||||
// Derived from the node key in the user config; available without the node
|
||||
// running.
|
||||
return result::toVariantMap(result::toLogosResult(m_blockchainClient->invokeRemoteMethod(
|
||||
BLOCKCHAIN_MODULE_NAME, QStringLiteral("get_peer_id"), userConfig())));
|
||||
}
|
||||
|
||||
QVariantMap BlockchainBackend::getClaimableVouchers()
|
||||
{
|
||||
if (!m_blockchainClient)
|
||||
return result::toVariantMap(result::err(QStringLiteral("Module not initialized.")));
|
||||
|
||||
return result::toVariantMap(result::toLogosResult(m_blockchainClient->invokeRemoteMethod(
|
||||
BLOCKCHAIN_MODULE_NAME, QStringLiteral("wallet_get_claimable_vouchers"))));
|
||||
}
|
||||
|
||||
void BlockchainBackend::startBlockchain()
|
||||
{
|
||||
if (!m_blockchainClient) {
|
||||
@ -423,9 +447,9 @@ QVariantMap BlockchainBackend::channelDepositWithNotes(
|
||||
args)));
|
||||
}
|
||||
|
||||
void BlockchainBackend::clearLogs()
|
||||
void BlockchainBackend::clearBlocks()
|
||||
{
|
||||
m_logModel->clear();
|
||||
m_blockModel->clear();
|
||||
}
|
||||
|
||||
void BlockchainBackend::copyToClipboard(QString text)
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
#include "rep_BlockchainBackend_source.h"
|
||||
|
||||
#include "AccountsModel.h"
|
||||
#include "LogModel.h"
|
||||
#include "BlockModel.h"
|
||||
|
||||
class LogosAPI;
|
||||
class LogosAPIClient;
|
||||
@ -20,22 +20,22 @@ class LogosAPIClient;
|
||||
// Inheriting from BlockchainBackendSimpleSource gives us the generated PROPs,
|
||||
// SLOTs and SIGNALs from BlockchainBackend.rep.
|
||||
//
|
||||
// AccountsModel* / LogModel* are subclass-only Q_PROPERTYs — QAbstractItemModel*
|
||||
// AccountsModel* / BlockModel* are subclass-only Q_PROPERTYs — QAbstractItemModel*
|
||||
// can't flow through a .rep, so ui-host auto-remotes each such property as
|
||||
// "<module>/<propertyName>" (see logos-view-module-runtime/ui-host/main.cpp).
|
||||
// QML acquires them via logos.model("blockchain_ui", "accounts"|"logs").
|
||||
// QML acquires them via logos.model("blockchain_ui", "accounts"|"blocks").
|
||||
class BlockchainBackend : public BlockchainBackendSimpleSource
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(AccountsModel* accounts READ accounts CONSTANT)
|
||||
Q_PROPERTY(LogModel* logs READ logs CONSTANT)
|
||||
Q_PROPERTY(BlockModel* blocks READ blocks CONSTANT)
|
||||
|
||||
public:
|
||||
explicit BlockchainBackend(LogosAPI* logosAPI, QObject* parent = nullptr);
|
||||
~BlockchainBackend() override;
|
||||
|
||||
AccountsModel* accounts() const { return m_accountsModel; }
|
||||
LogModel* logs() const { return m_logModel; }
|
||||
BlockModel* blocks() const { return m_blockModel; }
|
||||
|
||||
public slots:
|
||||
// Overrides of the pure-virtual slots generated from the .rep.
|
||||
@ -45,6 +45,9 @@ public slots:
|
||||
QVariantMap getBalance(QString addressHex) override;
|
||||
QVariantMap transferFunds(QString fromKeyHex, QString toKeyHex, QString amountStr) override;
|
||||
QVariantMap claimLeaderRewards() override;
|
||||
QVariantMap getCryptarchiaInfo() override;
|
||||
QVariantMap getPeerId() override;
|
||||
QVariantMap getClaimableVouchers() override;
|
||||
QVariantMap generateConfig(QString outputPath, QStringList initialPeers, int netPort,
|
||||
int blendPort, QString httpAddr, QString externalAddress,
|
||||
bool noPublicIpCheck, int deploymentMode,
|
||||
@ -57,7 +60,7 @@ public slots:
|
||||
QStringList fundingPublicKeyHexes,
|
||||
QString maxTxFee,
|
||||
QString optionalTipHex) override;
|
||||
void clearLogs() override;
|
||||
void clearBlocks() override;
|
||||
void copyToClipboard(QString text) override;
|
||||
|
||||
private:
|
||||
@ -67,7 +70,7 @@ private:
|
||||
LogosAPI* m_logosAPI = nullptr;
|
||||
LogosAPIClient* m_blockchainClient = nullptr;
|
||||
AccountsModel* m_accountsModel = nullptr;
|
||||
LogModel* m_logModel = nullptr;
|
||||
BlockModel* m_blockModel = nullptr;
|
||||
|
||||
static const QString BLOCKCHAIN_MODULE_NAME;
|
||||
};
|
||||
|
||||
@ -15,9 +15,12 @@ class BlockchainBackend
|
||||
SLOT(QVariantMap getBalance(QString addressHex))
|
||||
SLOT(QVariantMap transferFunds(QString fromKeyHex, QString toKeyHex, QString amountStr))
|
||||
SLOT(QVariantMap claimLeaderRewards())
|
||||
SLOT(QVariantMap getCryptarchiaInfo())
|
||||
SLOT(QVariantMap getPeerId())
|
||||
SLOT(QVariantMap getClaimableVouchers())
|
||||
SLOT(QVariantMap generateConfig(QString outputPath, QStringList initialPeers, int netPort, int blendPort, QString httpAddr, QString externalAddress, bool noPublicIpCheck, int deploymentMode, QString deploymentConfigPath, QString statePath))
|
||||
SLOT(QVariantMap getNotes(QString walletAddressHex, QString optionalTipHex))
|
||||
SLOT(QVariantMap channelDepositWithNotes(QString channelIdHex, QStringList inputNoteIdHexes, QString metadataBase58, QString changePublicKeyHex, QStringList fundingPublicKeyHexes, QString maxTxFee, QString optionalTipHex))
|
||||
SLOT(void clearLogs())
|
||||
SLOT(void clearBlocks())
|
||||
SLOT(void copyToClipboard(QString text))
|
||||
}
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
#include "LogModel.h"
|
||||
|
||||
int LogModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return m_lines.size();
|
||||
}
|
||||
|
||||
QVariant LogModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_lines.size())
|
||||
return QVariant();
|
||||
if (role == TextRole || role == Qt::DisplayRole)
|
||||
return m_lines.at(index.row());
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> LogModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> names;
|
||||
names[TextRole] = "text";
|
||||
return names;
|
||||
}
|
||||
|
||||
void LogModel::append(const QString& line)
|
||||
{
|
||||
const int row = m_lines.size();
|
||||
beginInsertRows(QModelIndex(), row, row);
|
||||
m_lines.append(line);
|
||||
endInsertRows();
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void LogModel::clear()
|
||||
{
|
||||
if (m_lines.isEmpty())
|
||||
return;
|
||||
beginResetModel();
|
||||
m_lines.clear();
|
||||
endResetModel();
|
||||
emit countChanged();
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QStringList>
|
||||
|
||||
class LogModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||
public:
|
||||
enum Roles { TextRole = Qt::UserRole + 1 };
|
||||
|
||||
explicit LogModel(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<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void append(const QString& line);
|
||||
Q_INVOKABLE void clear();
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
|
||||
private:
|
||||
QStringList m_lines;
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
@ -24,8 +25,10 @@ Rectangle {
|
||||
Connections {
|
||||
target: logos
|
||||
function onViewModuleReadyChanged(moduleName, isReady) {
|
||||
if (moduleName === "blockchain_ui")
|
||||
if (moduleName === "blockchain_ui") {
|
||||
root.ready = isReady && root.backend !== null
|
||||
if (root.ready) root.refreshPeerId()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,12 +36,48 @@ Rectangle {
|
||||
// Cover the case where the replica is already Valid by the time
|
||||
// we attach the Connections handler.
|
||||
root.ready = root.backend !== null && logos.isViewModuleReady("blockchain_ui")
|
||||
if (root.ready) root.refreshPeerId()
|
||||
}
|
||||
|
||||
// Graceful shutdown: if the window is closed while the node is running,
|
||||
// veto the close, stop the node, then close once it has stopped.
|
||||
property bool quitting: false
|
||||
|
||||
function _nodeBusy() {
|
||||
return root.backend
|
||||
&& (root.backend.status === BlockchainBackend.Running
|
||||
|| root.backend.status === BlockchainBackend.Starting
|
||||
|| root.backend.status === BlockchainBackend.Stopping)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.Window.window
|
||||
enabled: root.Window.window !== null
|
||||
ignoreUnknownSignals: true
|
||||
function onClosing(close) {
|
||||
if (!root.quitting && root._nodeBusy()) {
|
||||
root.quitting = true
|
||||
close.accepted = false
|
||||
root.backend.stopBlockchain()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once the stop initiated above completes, finish closing the window.
|
||||
Connections {
|
||||
target: root.backend
|
||||
enabled: root.quitting && root.backend !== null
|
||||
ignoreUnknownSignals: true
|
||||
function onStatusChanged() {
|
||||
if (root.quitting && !root._nodeBusy() && root.Window.window)
|
||||
root.Window.window.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Models live on the C++ backend and are auto-remoted by ui-host as
|
||||
// "<module>/<propertyName>". QML acquires them via logos.model(...).
|
||||
readonly property var accountsModel: logos.model("blockchain_ui", "accounts")
|
||||
readonly property var logModel: logos.model("blockchain_ui", "logs")
|
||||
readonly property var blockModel: logos.model("blockchain_ui", "blocks")
|
||||
|
||||
// Clipboard must be handled here in the UI-host (GUI) process. The backend
|
||||
// .rep source runs in a separate, non-GUI ViewModuleHost subprocess where
|
||||
@ -57,6 +96,90 @@ Rectangle {
|
||||
visible: false
|
||||
}
|
||||
|
||||
// Self libp2p peer id, derived from the selected user config (no running
|
||||
// node required). Refreshed when ready and whenever the config changes.
|
||||
property string peerId: ""
|
||||
|
||||
function refreshPeerId() {
|
||||
if (!root.backend || !root.backend.userConfig) {
|
||||
root.peerId = ""
|
||||
return
|
||||
}
|
||||
logos.watch(
|
||||
root.backend.getPeerId(),
|
||||
function(result) { root.peerId = result.success ? result.value : "" },
|
||||
function(error) { root.peerId = "" }
|
||||
)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
enabled: root.backend !== null
|
||||
ignoreUnknownSignals: true
|
||||
function onUserConfigChanged() { root.refreshPeerId() }
|
||||
}
|
||||
|
||||
// Live Cryptarchia consensus state, polled while the node runs.
|
||||
property string cryptarchiaInfoJson: ""
|
||||
property string cryptarchiaInfoError: ""
|
||||
|
||||
Timer {
|
||||
id: cryptarchiaTimer
|
||||
interval: 2000
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
running: root.ready && root.backend
|
||||
&& root.backend.status === BlockchainBackend.Running
|
||||
onTriggered: {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.getCryptarchiaInfo(),
|
||||
function(result) {
|
||||
if (result.success) {
|
||||
root.cryptarchiaInfoJson = result.value
|
||||
root.cryptarchiaInfoError = ""
|
||||
} else {
|
||||
root.cryptarchiaInfoError = _d.errorText(result.error)
|
||||
}
|
||||
},
|
||||
function(error) { root.cryptarchiaInfoError = _d.errorText(error) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Wallet's claimable ("pending") vouchers. Auto-refreshed on every incoming
|
||||
// block, and once when the node starts running.
|
||||
property string claimableVouchersJson: ""
|
||||
|
||||
function refreshClaimableVouchers() {
|
||||
if (!root.backend || root.backend.status !== BlockchainBackend.Running)
|
||||
return
|
||||
logos.watch(
|
||||
root.backend.getClaimableVouchers(),
|
||||
function(result) { if (result.success) root.claimableVouchersJson = result.value },
|
||||
function(error) { /* keep last known list on transient errors */ }
|
||||
)
|
||||
}
|
||||
|
||||
// Incoming blocks arrive as row insertions on the remoted block model.
|
||||
Connections {
|
||||
target: root.blockModel
|
||||
enabled: root.blockModel !== null
|
||||
ignoreUnknownSignals: true
|
||||
function onRowsInserted() { root.refreshClaimableVouchers() }
|
||||
}
|
||||
|
||||
// Initial load when the node reaches Running (before the next block).
|
||||
Connections {
|
||||
target: root.backend
|
||||
enabled: root.backend !== null
|
||||
ignoreUnknownSignals: true
|
||||
function onStatusChanged() {
|
||||
if (root.backend.status === BlockchainBackend.Running)
|
||||
root.refreshClaimableVouchers()
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: _d
|
||||
function errorText(message) {
|
||||
@ -170,18 +293,38 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Page 2: Node control, wallet, logs + Channel Deposit (tabbed)
|
||||
// Page 2: Node information + Wallet operations (tabbed)
|
||||
ColumnLayout {
|
||||
id: opPage
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Selected operation inside the Operations tab's sidebar nav.
|
||||
// 0 Accounts · 1 Transfer · 2 Leader Rewards · 3 Channel Deposit
|
||||
property int operationIndex: 0
|
||||
|
||||
// When pinned, Accounts stays visible (stacked on top) even while a
|
||||
// different operation is selected.
|
||||
property bool accountsPinned: false
|
||||
|
||||
readonly property bool nodeRunning: root.backend
|
||||
? root.backend.status === BlockchainBackend.Running
|
||||
: false
|
||||
|
||||
// Wallet operations require a running node. If the node stops while
|
||||
// the Operations tab is open, fall back to the Node tab so the user
|
||||
// isn't stranded on a disabled tab.
|
||||
onNodeRunningChanged: {
|
||||
if (!nodeRunning)
|
||||
operationTabBar.currentIndex = 0
|
||||
}
|
||||
|
||||
LogosTabBar {
|
||||
id: operationTabBar
|
||||
Layout.fillWidth: true
|
||||
LogosTabButton { text: qsTr("Node & Wallet") }
|
||||
LogosTabButton { text: qsTr("Node") }
|
||||
LogosTabButton {
|
||||
text: qsTr("Channel Deposit")
|
||||
enabled: root.backend
|
||||
&& root.backend.status === BlockchainBackend.Running
|
||||
text: qsTr("Operations")
|
||||
enabled: opPage.nodeRunning
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,149 +334,313 @@ Rectangle {
|
||||
Layout.fillHeight: true
|
||||
currentIndex: operationTabBar.currentIndex
|
||||
|
||||
SplitView {
|
||||
orientation: Qt.Vertical
|
||||
// ---- Tab 0: Node information (status + logs) ----
|
||||
SplitView {
|
||||
orientation: Qt.Vertical
|
||||
|
||||
ColumnLayout {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumHeight: 200
|
||||
spacing: Theme.spacing.large
|
||||
ColumnLayout {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumHeight: 120
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
StatusConfigView {
|
||||
Layout.fillWidth: true
|
||||
statusText: root.backend
|
||||
? _d.getStatusString(root.backend.status)
|
||||
: qsTr("Not Connected")
|
||||
statusColor: root.backend
|
||||
? _d.getStatusColor(root.backend.status)
|
||||
: Theme.palette.error
|
||||
userConfig: root.backend ? root.backend.userConfig : ""
|
||||
deploymentConfig: root.backend ? root.backend.deploymentConfig : ""
|
||||
useGeneratedConfig: root.backend ? root.backend.useGeneratedConfig : false
|
||||
canStart: root.backend
|
||||
&& !!root.backend.userConfig
|
||||
&& root.backend.status !== BlockchainBackend.Starting
|
||||
&& root.backend.status !== BlockchainBackend.Stopping
|
||||
isRunning: root.backend
|
||||
? root.backend.status === BlockchainBackend.Running
|
||||
: false
|
||||
StatusConfigView {
|
||||
Layout.fillWidth: true
|
||||
statusText: root.backend
|
||||
? _d.getStatusString(root.backend.status)
|
||||
: qsTr("Not Connected")
|
||||
statusColor: root.backend
|
||||
? _d.getStatusColor(root.backend.status)
|
||||
: Theme.palette.error
|
||||
userConfig: root.backend ? root.backend.userConfig : ""
|
||||
deploymentConfig: root.backend ? root.backend.deploymentConfig : ""
|
||||
useGeneratedConfig: root.backend ? root.backend.useGeneratedConfig : false
|
||||
canStart: root.backend
|
||||
&& !!root.backend.userConfig
|
||||
&& root.backend.status !== BlockchainBackend.Starting
|
||||
&& root.backend.status !== BlockchainBackend.Stopping
|
||||
isRunning: opPage.nodeRunning
|
||||
|
||||
onStartRequested: if (root.backend) root.backend.startBlockchain()
|
||||
onStopRequested: if (root.backend) root.backend.stopBlockchain()
|
||||
onChangeConfigRequested: _d.currentPage = 0
|
||||
}
|
||||
|
||||
WalletView {
|
||||
id: walletView
|
||||
accountsModel: root.accountsModel
|
||||
|
||||
onGetBalanceRequested: function(addressHex) {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.getBalance(addressHex),
|
||||
function(result) {
|
||||
if (result.success) {
|
||||
walletView.lastBalanceErrorAddress = ""
|
||||
walletView.lastBalanceError = ""
|
||||
} else {
|
||||
walletView.lastBalanceErrorAddress = addressHex
|
||||
walletView.lastBalanceError = _d.errorText(result.error)
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
walletView.lastBalanceErrorAddress = addressHex
|
||||
walletView.lastBalanceError = _d.errorText(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
onCopyToClipboard: (text) => {
|
||||
root.copyText(text)
|
||||
}
|
||||
onTransferRequested: function(fromKeyHex, toKeyHex, amount) {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.transferFunds(fromKeyHex, toKeyHex, amount),
|
||||
function(result) {
|
||||
if (result.success) {
|
||||
walletView.setTransferResult(result.value)
|
||||
} else {
|
||||
walletView.setTransferResult(_d.errorText(result.error))
|
||||
}
|
||||
},
|
||||
function(error) { walletView.setTransferResult(_d.errorText(error)) }
|
||||
)
|
||||
}
|
||||
onClaimLeaderRewardsRequested: function() {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.claimLeaderRewards(),
|
||||
function(result) {
|
||||
if (result.success) {
|
||||
walletView.setLeaderClaimResult(result.value)
|
||||
} else {
|
||||
walletView.setLeaderClaimResult(_d.errorText(result.error))
|
||||
}
|
||||
},
|
||||
function(error) { walletView.setLeaderClaimResult(_d.errorText(error)) }
|
||||
)
|
||||
}
|
||||
onRefreshAccountsRequested: if (root.backend) root.backend.refreshAccounts()
|
||||
onStartRequested: if (root.backend) root.backend.startBlockchain()
|
||||
onStopRequested: if (root.backend) root.backend.stopBlockchain()
|
||||
onChangeConfigRequested: _d.currentPage = 0
|
||||
}
|
||||
|
||||
NodeInfoView {
|
||||
Layout.fillWidth: true
|
||||
peerId: root.peerId
|
||||
onCopyToClipboard: (text) => root.copyText(text)
|
||||
}
|
||||
|
||||
CryptarchiaInfoView {
|
||||
Layout.fillWidth: true
|
||||
visible: opPage.nodeRunning
|
||||
infoJson: root.cryptarchiaInfoJson
|
||||
errorText: root.cryptarchiaInfoError
|
||||
onCopyToClipboard: (text) => root.copyText(text)
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredHeight: Theme.spacing.small
|
||||
}
|
||||
}
|
||||
|
||||
BlocksView {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumHeight: 150
|
||||
|
||||
blockModel: root.blockModel
|
||||
onClearRequested: if (root.backend) root.backend.clearBlocks()
|
||||
onCopyToClipboard: (text) => {
|
||||
root.copyText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Tab 1: Wallet operations (sidebar nav + panels) ----
|
||||
// Anchor-based (not a Layout): StackLayout force-fills this Item,
|
||||
// and anchors give the SplitView explicit geometry. The panels
|
||||
// have ~zero implicit height, so a plain Layout would collapse
|
||||
// them — anchors + SplitView.fillHeight avoid that.
|
||||
Item {
|
||||
Layout.preferredHeight: Theme.spacing.small
|
||||
// Sidebar navigation
|
||||
ColumnLayout {
|
||||
id: opSidebar
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: 180
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
NavItem { label: qsTr("Accounts"); index: 0; pinnable: true }
|
||||
NavItem { label: qsTr("Transfer"); index: 1 }
|
||||
NavItem { label: qsTr("Leader Rewards"); index: 2 }
|
||||
NavItem { label: qsTr("Channel Deposit"); index: 3 }
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: opDivider
|
||||
anchors.left: opSidebar.right
|
||||
anchors.leftMargin: Theme.spacing.large
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: 1
|
||||
color: Theme.palette.borderSecondary
|
||||
}
|
||||
|
||||
// Operation panels. Accounts lives outside the stack so it
|
||||
// can stay pinned on top while another operation is shown;
|
||||
// a vertical SplitView keeps both visible and resizable.
|
||||
SplitView {
|
||||
anchors.left: opDivider.right
|
||||
anchors.leftMargin: Theme.spacing.large
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
orientation: Qt.Vertical
|
||||
|
||||
AccountsView {
|
||||
id: accountsView
|
||||
visible: opPage.operationIndex === 0 || opPage.accountsPinned
|
||||
// Fills when it's the sole panel; when pinned beside
|
||||
// an operation it's a resizable 260px strip on top
|
||||
// (the operation below is the SplitView filler).
|
||||
SplitView.fillHeight: opPage.operationIndex === 0
|
||||
SplitView.preferredHeight: 260
|
||||
SplitView.minimumHeight: 120
|
||||
|
||||
accountsModel: root.accountsModel
|
||||
|
||||
onGetBalanceRequested: function(addressHex) {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.getBalance(addressHex),
|
||||
function(result) {
|
||||
if (result.success) {
|
||||
accountsView.lastBalanceErrorAddress = ""
|
||||
accountsView.lastBalanceError = ""
|
||||
} else {
|
||||
accountsView.lastBalanceErrorAddress = addressHex
|
||||
accountsView.lastBalanceError = _d.errorText(result.error)
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
accountsView.lastBalanceErrorAddress = addressHex
|
||||
accountsView.lastBalanceError = _d.errorText(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
onRefreshAccountsRequested: if (root.backend) root.backend.refreshAccounts()
|
||||
onCopyToClipboard: (text) => {
|
||||
root.copyText(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer / Leader Rewards / Channel Deposit.
|
||||
// operationIndex 1,2,3 maps to stack index 0,1,2.
|
||||
StackLayout {
|
||||
id: otherOpsStack
|
||||
SplitView.fillHeight: true
|
||||
SplitView.minimumHeight: 150
|
||||
visible: opPage.operationIndex !== 0
|
||||
currentIndex: Math.max(0, opPage.operationIndex - 1)
|
||||
|
||||
TransferView {
|
||||
id: transferView
|
||||
accountsModel: root.accountsModel
|
||||
|
||||
onTransferRequested: function(fromKeyHex, toKeyHex, amount) {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.transferFunds(fromKeyHex, toKeyHex, amount),
|
||||
function(result) {
|
||||
if (result.success) {
|
||||
transferView.setTransferResult(result.value)
|
||||
} else {
|
||||
transferView.setTransferResult(_d.errorText(result.error))
|
||||
}
|
||||
},
|
||||
function(error) { transferView.setTransferResult(_d.errorText(error)) }
|
||||
)
|
||||
}
|
||||
onCopyToClipboard: (text) => {
|
||||
root.copyText(text)
|
||||
}
|
||||
}
|
||||
|
||||
LeaderRewardsView {
|
||||
id: leaderRewardsView
|
||||
vouchersJson: root.claimableVouchersJson
|
||||
|
||||
onClaimLeaderRewardsRequested: function() {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.claimLeaderRewards(),
|
||||
function(result) {
|
||||
if (result.success) {
|
||||
leaderRewardsView.setLeaderClaimResult(result.value)
|
||||
} else {
|
||||
leaderRewardsView.setLeaderClaimResult(_d.errorText(result.error))
|
||||
}
|
||||
// Reflect the claim in the pending list.
|
||||
root.refreshClaimableVouchers()
|
||||
},
|
||||
function(error) { leaderRewardsView.setLeaderClaimResult(_d.errorText(error)) }
|
||||
)
|
||||
}
|
||||
onCopyToClipboard: (text) => {
|
||||
root.copyText(text)
|
||||
}
|
||||
}
|
||||
|
||||
ChannelDepositView {
|
||||
id: channelDepositView
|
||||
accountsModel: root.accountsModel
|
||||
nodeRunning: opPage.nodeRunning
|
||||
|
||||
onGetNotesRequested: function(addressHex, optionalTipHex) {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.getNotes(addressHex, optionalTipHex),
|
||||
function(result) {
|
||||
if (result.success)
|
||||
channelDepositView.setNotes(result.value)
|
||||
else
|
||||
channelDepositView.setNotesError(_d.errorText(result.error))
|
||||
},
|
||||
function(error) { channelDepositView.setNotesError(_d.errorText(error)) }
|
||||
)
|
||||
}
|
||||
onSubmitRequested: function(channelIdHex, inputNoteIdHexes, metadataBase58, changePublicKeyHex, fundingPublicKeyHexes, maxTxFee, optionalTipHex) {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.channelDepositWithNotes(
|
||||
channelIdHex, inputNoteIdHexes, metadataBase58,
|
||||
changePublicKeyHex, fundingPublicKeyHexes, maxTxFee, optionalTipHex),
|
||||
function(result) {
|
||||
if (result.success)
|
||||
channelDepositView.setSubmitResult(true, result.value)
|
||||
else
|
||||
channelDepositView.setSubmitResult(false, _d.errorText(result.error))
|
||||
},
|
||||
function(error) { channelDepositView.setSubmitResult(false, _d.errorText(error)) }
|
||||
)
|
||||
}
|
||||
onCopyToClipboard: (text) => {
|
||||
root.copyText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogsView {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumHeight: 150
|
||||
// Sidebar nav entry used by the Operations tab. `pinnable` adds a
|
||||
// pin toggle on the right (used by Accounts) that keeps the panel
|
||||
// visible alongside other operations.
|
||||
component NavItem: Rectangle {
|
||||
property string label
|
||||
property int index
|
||||
property bool pinnable: false
|
||||
|
||||
logModel: root.logModel
|
||||
onClearRequested: if (root.backend) root.backend.clearLogs()
|
||||
onCopyToClipboard: (text) => {
|
||||
root.copyText(text)
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 40
|
||||
radius: Theme.spacing.radiusSmall
|
||||
color: opPage.operationIndex === index
|
||||
? Theme.palette.backgroundTertiary
|
||||
: (navMouse.containsMouse ? Theme.palette.backgroundSecondary : "transparent")
|
||||
|
||||
// Background click selects the operation. Sits below the row so
|
||||
// the pin button on top captures its own clicks.
|
||||
MouseArea {
|
||||
id: navMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: opPage.operationIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChannelDepositView {
|
||||
id: channelDepositView
|
||||
accountsModel: root.accountsModel
|
||||
nodeRunning: root.backend
|
||||
? root.backend.status === BlockchainBackend.Running
|
||||
: false
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacing.medium
|
||||
anchors.rightMargin: Theme.spacing.small
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
onGetNotesRequested: function(addressHex, optionalTipHex) {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.getNotes(addressHex, optionalTipHex),
|
||||
function(result) {
|
||||
if (result.success)
|
||||
channelDepositView.setNotes(result.value)
|
||||
else
|
||||
channelDepositView.setNotesError(_d.errorText(result.error))
|
||||
},
|
||||
function(error) { channelDepositView.setNotesError(_d.errorText(error)) }
|
||||
)
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: label
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: opPage.operationIndex === index
|
||||
color: opPage.operationIndex === index
|
||||
? Theme.palette.primary
|
||||
: Theme.palette.text
|
||||
}
|
||||
onSubmitRequested: function(channelIdHex, inputNoteIdHexes, metadataBase58, changePublicKeyHex, fundingPublicKeyHexes, maxTxFee, optionalTipHex) {
|
||||
if (!root.backend) return
|
||||
logos.watch(
|
||||
root.backend.channelDepositWithNotes(
|
||||
channelIdHex, inputNoteIdHexes, metadataBase58,
|
||||
changePublicKeyHex, fundingPublicKeyHexes, maxTxFee, optionalTipHex),
|
||||
function(result) {
|
||||
if (result.success)
|
||||
channelDepositView.setSubmitResult(true, result.value)
|
||||
else
|
||||
channelDepositView.setSubmitResult(false, _d.errorText(result.error))
|
||||
},
|
||||
function(error) { channelDepositView.setSubmitResult(false, _d.errorText(error)) }
|
||||
)
|
||||
}
|
||||
onCopyToClipboard: (text) => {
|
||||
root.copyText(text)
|
||||
|
||||
// Pin toggle (Accounts only). A flat icon button matching
|
||||
// the other SVG icons; the pin colours up when pinned. Its
|
||||
// own click handling stops the nav-background MouseArea
|
||||
// below from also selecting the item.
|
||||
Button {
|
||||
visible: pinnable
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: 28
|
||||
Layout.preferredHeight: 28
|
||||
display: AbstractButton.IconOnly
|
||||
flat: true
|
||||
padding: 4
|
||||
icon.source: Qt.resolvedUrl("icons/pin.svg")
|
||||
icon.width: 18
|
||||
icon.height: 18
|
||||
icon.color: opPage.accountsPinned
|
||||
? Theme.palette.primary
|
||||
: Theme.palette.textTertiary
|
||||
onClicked: opPage.accountsPinned = !opPage.accountsPinned
|
||||
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: opPage.accountsPinned
|
||||
? qsTr("Unpin accounts") : qsTr("Pin accounts")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
207
src/qml/controls/BlockDelegate.qml
Normal file
207
src/qml/controls/BlockDelegate.qml
Normal file
@ -0,0 +1,207 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// Collapsible card for a single block (BlockModel row).
|
||||
// Collapsed: timestamp · slot · version · tx count
|
||||
// Expanded: header hashes, proof-of-leadership group, transactions list
|
||||
// Unparsed payloads fall back to showing their raw text.
|
||||
Rectangle {
|
||||
id: del
|
||||
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
property bool expanded: false
|
||||
property bool proofExpanded: false
|
||||
|
||||
// The transactions role is a QStringList; surface it for the Repeater.
|
||||
readonly property var transactionsList: model.transactions || []
|
||||
|
||||
// Roles read as `undefined` during the brief QtRO replica sync at node
|
||||
// startup. Treat the fallback as active ONLY when the model says so
|
||||
// explicitly (parsed === false); undefined means "still loading", not
|
||||
// "unparsed" — otherwise freshly-arrived blocks flash as Unparsed.
|
||||
readonly property bool isUnparsed: model.parsed === false
|
||||
|
||||
width: ListView.view ? ListView.view.width : implicitWidth
|
||||
implicitHeight: col.implicitHeight + 2 * Theme.spacing.medium
|
||||
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.medium
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
// ---- Summary row (always visible, toggles expansion) ----
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: del.expanded ? "▾" : "▸"
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: model.timestamp || ""
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
LogosText {
|
||||
visible: !del.isUnparsed
|
||||
text: qsTr("slot %1").arg(model.slot || qsTr("?"))
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
// Version badge
|
||||
Rectangle {
|
||||
visible: !del.isUnparsed && (model.version || "").length > 0
|
||||
radius: Theme.spacing.radiusSmall
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
implicitWidth: versionText.implicitWidth + 2 * Theme.spacing.small
|
||||
implicitHeight: versionText.implicitHeight + Theme.spacing.tiny
|
||||
LogosText {
|
||||
id: versionText
|
||||
anchors.centerIn: parent
|
||||
text: model.version || ""
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
|
||||
LogosText {
|
||||
visible: del.isUnparsed
|
||||
text: qsTr("Unparsed block")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.warning
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
LogosText {
|
||||
visible: !del.isUnparsed
|
||||
text: qsTr("%1 tx").arg(model.txCount || 0)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
TapHandler { onTapped: del.expanded = !del.expanded }
|
||||
}
|
||||
|
||||
// ---- Expanded details ----
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: del.expanded
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
color: Theme.palette.borderSecondary
|
||||
}
|
||||
|
||||
// Parsed: structured header
|
||||
HashRow {
|
||||
label: qsTr("Parent block"); value: model.parentBlock || ""; visible: !del.isUnparsed
|
||||
onCopyRequested: (t) => del.copyToClipboard(t)
|
||||
}
|
||||
HashRow {
|
||||
label: qsTr("Block root"); value: model.blockRoot || ""; visible: !del.isUnparsed
|
||||
onCopyRequested: (t) => del.copyToClipboard(t)
|
||||
}
|
||||
HashRow {
|
||||
label: qsTr("Signature"); value: model.signature || ""; visible: !del.isUnparsed
|
||||
onCopyRequested: (t) => del.copyToClipboard(t)
|
||||
}
|
||||
|
||||
// Proof of leadership (collapsible sub-group)
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: !del.isUnparsed
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: (del.proofExpanded ? "▾ " : "▸ ") + qsTr("Proof of leadership")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
TapHandler { onTapped: del.proofExpanded = !del.proofExpanded }
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Theme.spacing.medium
|
||||
visible: !del.isUnparsed && del.proofExpanded
|
||||
spacing: Theme.spacing.small
|
||||
HashRow { label: qsTr("Leader key"); value: model.leaderKey || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
|
||||
HashRow { label: qsTr("Entropy"); value: model.entropy || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
|
||||
HashRow { label: qsTr("Proof"); value: model.proof || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
|
||||
HashRow { label: qsTr("Voucher cm"); value: model.voucherCm || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
|
||||
}
|
||||
|
||||
// Transactions
|
||||
LogosText {
|
||||
visible: !del.isUnparsed
|
||||
text: qsTr("Transactions (%1)").arg(model.txCount || 0)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: !del.isUnparsed
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
Repeater {
|
||||
model: del.expanded ? del.transactionsList : []
|
||||
delegate: TransactionDelegate {
|
||||
required property int index
|
||||
required property string modelData
|
||||
Layout.fillWidth: true
|
||||
idx: index
|
||||
json: modelData
|
||||
onCopyToClipboard: (t) => del.copyToClipboard(t)
|
||||
}
|
||||
}
|
||||
|
||||
LogosText {
|
||||
visible: (model.txCount || 0) === 0
|
||||
text: qsTr("No transactions in this block.")
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
}
|
||||
|
||||
// Unparsed: raw fallback
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: del.isUnparsed
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: qsTr("Raw payload")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosCopyButton { onCopyText: del.copyToClipboard(model.rawJson || "") }
|
||||
}
|
||||
JsonBlock {
|
||||
Layout.fillWidth: true
|
||||
visible: del.isUnparsed
|
||||
json: model.rawJson || qsTr("(no payload)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/qml/controls/HashRow.qml
Normal file
41
src/qml/controls/HashRow.qml
Normal file
@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// A labelled value row with an elided monospace value and a copy button.
|
||||
// Used for hashes/keys and any short scalar field.
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
property int labelWidth: 110
|
||||
property bool copyable: true
|
||||
|
||||
signal copyRequested(string text)
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
visible: root.label.length > 0
|
||||
text: root.label
|
||||
Layout.preferredWidth: root.labelWidth
|
||||
Layout.alignment: Qt.AlignTop
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: root.value && root.value.length > 0 ? root.value : "—"
|
||||
elide: Text.ElideMiddle
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: "monospace"
|
||||
}
|
||||
LogosCopyButton {
|
||||
visible: root.copyable && root.value && root.value.length > 0
|
||||
onCopyText: root.copyRequested(root.value)
|
||||
}
|
||||
}
|
||||
59
src/qml/controls/InfoButton.qml
Normal file
59
src/qml/controls/InfoButton.qml
Normal file
@ -0,0 +1,59 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// Small circled-"i" help button. Click to toggle a popup with `text`, a short
|
||||
// description of the operation. Styled to match the other SVG icon buttons.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
|
||||
implicitWidth: 28
|
||||
implicitHeight: 28
|
||||
|
||||
Button {
|
||||
id: btn
|
||||
anchors.fill: parent
|
||||
display: AbstractButton.IconOnly
|
||||
flat: true
|
||||
padding: 4
|
||||
icon.source: Qt.resolvedUrl("../icons/info.svg")
|
||||
icon.width: 18
|
||||
icon.height: 18
|
||||
icon.color: (btn.hovered || popup.visible)
|
||||
? Theme.palette.primary
|
||||
: Theme.palette.textTertiary
|
||||
onClicked: popup.visible ? popup.close() : popup.open()
|
||||
|
||||
ToolTip.visible: btn.hovered && !popup.visible
|
||||
ToolTip.text: qsTr("What is this?")
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popup
|
||||
// Right-aligned to the button, opening downward.
|
||||
x: root.width - width
|
||||
y: root.height + Theme.spacing.tiny
|
||||
width: 300
|
||||
padding: Theme.spacing.medium
|
||||
modal: false
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
radius: Theme.spacing.radiusLarge
|
||||
}
|
||||
contentItem: LogosText {
|
||||
text: root.text
|
||||
wrapMode: Text.WordWrap
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.text
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/qml/controls/JsonBlock.qml
Normal file
32
src/qml/controls/JsonBlock.qml
Normal file
@ -0,0 +1,32 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// A boxed, monospace, wrapping JSON/text block. Used for prettified payloads
|
||||
// and raw fallbacks.
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string json: ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: jsonText.implicitHeight + 2 * Theme.spacing.small
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusSmall
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
LogosText {
|
||||
id: jsonText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.small
|
||||
text: root.json
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: "monospace"
|
||||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
}
|
||||
180
src/qml/controls/TransactionDelegate.qml
Normal file
180
src/qml/controls/TransactionDelegate.qml
Normal file
@ -0,0 +1,180 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// Structured view of a single block transaction (a SignedMantleTx JSON string).
|
||||
//
|
||||
// Shape (see nomos-node core/src/mantle):
|
||||
// { "mantle_tx": { "ops": [ {opcode, payload}, ... ] },
|
||||
// "ops_proofs": [ <proof>, ... ] } // paired with ops by index
|
||||
//
|
||||
// Each op is labelled by its opcode name; the payload (and paired proof) are
|
||||
// shown as prettified JSON. Unparsable transactions fall back to raw JSON.
|
||||
ColumnLayout {
|
||||
id: txRoot
|
||||
|
||||
property string json: ""
|
||||
property int idx: 0
|
||||
property bool open: false
|
||||
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
readonly property var parsed: safeParse(json)
|
||||
readonly property bool parseFailed: parsed === null
|
||||
readonly property var ops: (parsed && parsed.mantle_tx && parsed.mantle_tx.ops)
|
||||
? parsed.mantle_tx.ops : []
|
||||
readonly property var proofs: (parsed && parsed.ops_proofs) ? parsed.ops_proofs : []
|
||||
|
||||
// Transaction id, if the block payload includes one. Falls back to the
|
||||
// positional "Transaction N" label when absent.
|
||||
readonly property string txId: (parsed && parsed.id) ? String(parsed.id) : ""
|
||||
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
function safeParse(s) {
|
||||
try { return JSON.parse(s) } catch (e) { return null }
|
||||
}
|
||||
|
||||
function opName(code) {
|
||||
switch (code) {
|
||||
case 0: return qsTr("Transfer")
|
||||
case 16: return qsTr("Channel Config")
|
||||
case 17: return qsTr("Channel Inscribe")
|
||||
case 18: return qsTr("Channel Deposit")
|
||||
case 19: return qsTr("Channel Withdraw")
|
||||
case 32: return qsTr("SDP Declare")
|
||||
case 33: return qsTr("SDP Withdraw")
|
||||
case 34: return qsTr("SDP Active")
|
||||
case 48: return qsTr("Leader Claim")
|
||||
default: return qsTr("Op 0x%1").arg(Number(code).toString(16))
|
||||
}
|
||||
}
|
||||
|
||||
function pretty(v) {
|
||||
try { return JSON.stringify(v, null, 2) } catch (e) { return String(v) }
|
||||
}
|
||||
|
||||
function proofVariant(p) {
|
||||
if (!p || typeof p !== "object") return ""
|
||||
var ks = Object.keys(p)
|
||||
return ks.length ? ks[0] : ""
|
||||
}
|
||||
|
||||
// ---- Header (toggle) ----
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: txRoot.open ? "▾" : "▸"
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
TapHandler { onTapped: txRoot.open = !txRoot.open }
|
||||
}
|
||||
LogosText {
|
||||
// Show the tx id when present, else the positional label.
|
||||
Layout.fillWidth: true
|
||||
text: txRoot.txId !== "" ? txRoot.txId : qsTr("Transaction %1").arg(txRoot.idx + 1)
|
||||
elide: Text.ElideMiddle
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
font.family: txRoot.txId !== "" ? "monospace" : Theme.typography.publicSans
|
||||
TapHandler { onTapped: txRoot.open = !txRoot.open }
|
||||
}
|
||||
LogosCopyButton {
|
||||
// Copy the tx id when available, otherwise the full tx JSON.
|
||||
onCopyText: txRoot.copyToClipboard(txRoot.txId !== "" ? txRoot.txId : txRoot.json)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Body ----
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Theme.spacing.medium
|
||||
visible: txRoot.open
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
JsonBlock {
|
||||
Layout.fillWidth: true
|
||||
visible: txRoot.parseFailed
|
||||
json: txRoot.json
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: txRoot.open && !txRoot.parseFailed ? txRoot.ops : []
|
||||
delegate: OpView {
|
||||
required property int index
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
op: modelData
|
||||
proof: txRoot.proofs.length > index ? txRoot.proofs[index] : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Inline: one operation (opcode name + payload/proof JSON) ----
|
||||
component OpView: ColumnLayout {
|
||||
id: opView
|
||||
property var op
|
||||
property var proof: null
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
Rectangle {
|
||||
radius: Theme.spacing.radiusSmall
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
implicitWidth: opcodeText.implicitWidth + 2 * Theme.spacing.small
|
||||
implicitHeight: opcodeText.implicitHeight + Theme.spacing.tiny
|
||||
LogosText {
|
||||
id: opcodeText
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("op %1").arg(opView.op && opView.op.opcode !== undefined ? opView.op.opcode : "?")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: txRoot.opName(opView.op ? opView.op.opcode : -1)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
LogosCopyButton {
|
||||
onCopyText: txRoot.copyToClipboard(txRoot.pretty(opView.op ? opView.op.payload : null))
|
||||
}
|
||||
}
|
||||
|
||||
// payload as JSON
|
||||
JsonBlock {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Theme.spacing.medium
|
||||
json: txRoot.pretty(opView.op ? opView.op.payload : null)
|
||||
}
|
||||
|
||||
// paired proof
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Theme.spacing.medium
|
||||
visible: opView.proof !== null && opView.proof !== undefined
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: qsTr("Proof · %1").arg(txRoot.proofVariant(opView.proof) || qsTr("unknown"))
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosCopyButton { onCopyText: txRoot.copyToClipboard(txRoot.pretty(opView.proof)) }
|
||||
}
|
||||
JsonBlock {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Theme.spacing.medium
|
||||
visible: opView.proof !== null && opView.proof !== undefined
|
||||
json: (opView.proof !== null && opView.proof !== undefined) ? txRoot.pretty(opView.proof) : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/qml/icons/info.svg
Normal file
1
src/qml/icons/info.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m11 7h2v2h-2zm0 4h2v6h-2zm1-9c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 278 B |
1
src/qml/icons/pin.svg
Normal file
1
src/qml/icons/pin.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m16 12v-8h1v-2h-10v2h1v8l-2 2v2h5.2v6h1.6v-6h5.2v-2z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 178 B |
83
src/qml/views/AccountsView.qml
Normal file
83
src/qml/views/AccountsView.qml
Normal file
@ -0,0 +1,83 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
|
||||
// Accounts panel: the list of known wallet addresses with per-account
|
||||
// balance refresh and copy. Extracted from the former WalletView.
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property var accountsModel
|
||||
|
||||
property string lastBalanceError: ""
|
||||
property string lastBalanceErrorAddress: ""
|
||||
|
||||
signal getBalanceRequested(string addressHex)
|
||||
signal refreshAccountsRequested()
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
LogosText {
|
||||
text: qsTr("Accounts")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosButton {
|
||||
text: qsTr("Refresh")
|
||||
padding: Theme.spacing.small
|
||||
onClicked: root.refreshAccountsRequested()
|
||||
}
|
||||
InfoButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: qsTr("Your wallet addresses and balances. Press Refresh to fetch the latest known addresses and their balances from the running node.")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: balanceListView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: root.accountsModel
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
delegate: AccountDelegate {
|
||||
balanceError: root.lastBalanceErrorAddress === model.address ?
|
||||
root.lastBalanceError : ""
|
||||
onGetBalanceRequested: (addr) => root.getBalanceRequested(addr)
|
||||
onCopyRequested: (text) => root.copyToClipboard(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/qml/views/BlocksView.qml
Normal file
85
src/qml/views/BlocksView.qml
Normal file
@ -0,0 +1,85 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
|
||||
// Structured view of recent blocks (BlockModel). Newest block is at the top;
|
||||
// only the latest 100 are retained by the model.
|
||||
Control {
|
||||
id: root
|
||||
|
||||
// --- Public API ---
|
||||
required property var blockModel
|
||||
|
||||
signal clearRequested()
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.background
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacing.large
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Blocks")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
LogosButton {
|
||||
text: qsTr("Clear")
|
||||
Layout.preferredWidth: 80
|
||||
Layout.preferredHeight: 32
|
||||
onClicked: root.clearRequested()
|
||||
}
|
||||
}
|
||||
|
||||
// Block list
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ListView {
|
||||
id: blocksListView
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacing.small
|
||||
clip: true
|
||||
model: root.blockModel
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
delegate: BlockDelegate {
|
||||
onCopyToClipboard: (text) => root.copyToClipboard(text)
|
||||
}
|
||||
|
||||
LogosText {
|
||||
// ListView's `count` has a NOTIFY signal, unlike the remoted
|
||||
// model's own count property — use it for the empty state.
|
||||
visible: blocksListView.count === 0
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("No blocks yet...")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,11 +66,32 @@ ColumnLayout {
|
||||
readonly property int stepCount: 4
|
||||
property string notesTip: ""
|
||||
|
||||
// Address whose notes are currently loaded. Selecting/entering a
|
||||
// different address clears the old notes and loads the new ones.
|
||||
property string loadedAddress: ""
|
||||
|
||||
// result state
|
||||
property bool resultPending: false
|
||||
property bool resultSuccess: false
|
||||
property string resultText: ""
|
||||
|
||||
// Clear any loaded notes and (re)load for `addr` — but only when it
|
||||
// actually differs from what's already loaded.
|
||||
function loadNotesFor(addr) {
|
||||
var a = (addr || "").trim()
|
||||
if (a === "" || a === loadedAddress)
|
||||
return
|
||||
loadedAddress = a
|
||||
noteSelector.clearSelection()
|
||||
noteSelector.notes = []
|
||||
noteSelector.errorText = ""
|
||||
notesTip = ""
|
||||
if (!root.nodeRunning)
|
||||
return
|
||||
root.setNotesLoading()
|
||||
root.getNotesRequested(a, "")
|
||||
}
|
||||
|
||||
function fundingKeyList() {
|
||||
return fundingKeysArea.text.split("\n")
|
||||
.map(function(s) { return s.trim() })
|
||||
@ -143,6 +164,8 @@ ColumnLayout {
|
||||
noteSelector.clearSelection()
|
||||
noteSelector.notes = []
|
||||
noteSelector.errorText = ""
|
||||
walletField.text = ""
|
||||
loadedAddress = ""
|
||||
channelIdField.text = ""
|
||||
changeKeyField.text = ""
|
||||
fundingKeysArea.text = ""
|
||||
@ -186,6 +209,10 @@ ColumnLayout {
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
InfoButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: qsTr("Deposit wallet notes (UTXOs) into a channel. Pick an address to load its notes, select the notes to consume, fill in the channel id, change/funding keys and max fee, then confirm to submit.")
|
||||
}
|
||||
}
|
||||
|
||||
LogosText {
|
||||
@ -224,19 +251,23 @@ ColumnLayout {
|
||||
model: root.accountsModel
|
||||
textRole: "address"
|
||||
currentIndex: -1
|
||||
onActivated: function(index) { walletField.text = currentText }
|
||||
// Selecting a (different) known address loads its notes.
|
||||
onActivated: function(index) {
|
||||
walletField.text = currentText
|
||||
d.loadNotesFor(currentText)
|
||||
}
|
||||
}
|
||||
LogosTextField {
|
||||
id: walletField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Wallet address hex")
|
||||
}
|
||||
LogosButton {
|
||||
text: qsTr("Load notes")
|
||||
enabled: root.nodeRunning && walletField.text.trim().length > 0
|
||||
onClicked: {
|
||||
root.setNotesLoading()
|
||||
root.getNotesRequested(walletField.text.trim(), "")
|
||||
|
||||
// LogosTextField has no editingFinished; reach the inner
|
||||
// TextInput. Manually entered address loads on commit
|
||||
// (Enter / focus out).
|
||||
Connections {
|
||||
target: walletField.textInput
|
||||
function onEditingFinished() { d.loadNotesFor(walletField.text) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
143
src/qml/views/CryptarchiaInfoView.qml
Normal file
143
src/qml/views/CryptarchiaInfoView.qml
Normal file
@ -0,0 +1,143 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
|
||||
// Live Cryptarchia consensus state, polled from get_cryptarchia_info.
|
||||
// The result `value` is a JSON string; parsed defensively (nested under
|
||||
// "cryptarchia_info" or flat, mode as string or numeric enum).
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string infoJson: ""
|
||||
property string errorText: ""
|
||||
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
readonly property var info: parse(infoJson)
|
||||
|
||||
function parse(s) {
|
||||
try { return s && s.length > 0 ? JSON.parse(s) : null } catch (e) { return null }
|
||||
}
|
||||
|
||||
function field(key) {
|
||||
if (!info) return undefined
|
||||
if (info.cryptarchia_info && info.cryptarchia_info[key] !== undefined)
|
||||
return info.cryptarchia_info[key]
|
||||
return info[key]
|
||||
}
|
||||
|
||||
function num(key) {
|
||||
var v = field(key)
|
||||
return (v === undefined || v === null) ? qsTr("—") : String(v)
|
||||
}
|
||||
|
||||
function hash(key) {
|
||||
var v = field(key)
|
||||
return (v === undefined || v === null) ? "" : String(v)
|
||||
}
|
||||
|
||||
// mode lives at the top level (ChainServiceMode); accept string or the
|
||||
// c-binding numeric enum (0 Bootstrapping / 1 Online / 2 NotStarted).
|
||||
function modeText() {
|
||||
var m = info ? info.mode : undefined
|
||||
if (m === undefined || m === null) return qsTr("—")
|
||||
if (typeof m === "number") {
|
||||
switch (m) {
|
||||
case 0: return qsTr("Bootstrapping")
|
||||
case 1: return qsTr("Online")
|
||||
case 2: return qsTr("Not Started")
|
||||
default: return String(m)
|
||||
}
|
||||
}
|
||||
return String(m)
|
||||
}
|
||||
|
||||
function modeColor() {
|
||||
var t = modeText()
|
||||
if (t === qsTr("Online")) return Theme.palette.success
|
||||
if (t === qsTr("Bootstrapping")) return Theme.palette.warning
|
||||
return Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
implicitHeight: contentCol.implicitHeight + 2 * Theme.spacing.large
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: contentCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: qsTr("Consensus")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosText {
|
||||
text: root.modeText()
|
||||
color: root.modeColor()
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
visible: root.errorText.length > 0
|
||||
text: root.errorText
|
||||
color: Theme.palette.error
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.errorText.length === 0
|
||||
spacing: Theme.spacing.large
|
||||
LogosText {
|
||||
text: qsTr("Slot: %1").arg(root.num("slot"))
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
LogosText {
|
||||
text: qsTr("Height: %1").arg(root.num("height"))
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
LogosText {
|
||||
visible: root.field("lib_slot") !== undefined
|
||||
text: qsTr("LIB slot: %1").arg(root.num("lib_slot"))
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
HashRow {
|
||||
visible: root.errorText.length === 0
|
||||
label: qsTr("Tip")
|
||||
value: root.hash("tip")
|
||||
onCopyRequested: (t) => root.copyToClipboard(t)
|
||||
}
|
||||
HashRow {
|
||||
visible: root.errorText.length === 0
|
||||
label: qsTr("LIB")
|
||||
value: root.hash("lib")
|
||||
onCopyRequested: (t) => root.copyToClipboard(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
228
src/qml/views/LeaderRewardsView.qml
Normal file
228
src/qml/views/LeaderRewardsView.qml
Normal file
@ -0,0 +1,228 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
|
||||
// Leader rewards panel: claim action plus a read-only, horizontally-sliding
|
||||
// list of the wallet's claimable ("pending") vouchers with a count. The
|
||||
// protocol picks which voucher a claim consumes — the list is informational.
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// JSON from wallet_get_claimable_vouchers:
|
||||
// { "tip": "<hex>", "vouchers": [ {commitment, nullifier}, ... ] }
|
||||
property string vouchersJson: ""
|
||||
|
||||
signal claimLeaderRewardsRequested()
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
function setLeaderClaimResult(text) {
|
||||
leaderClaimResultText.text = text
|
||||
}
|
||||
|
||||
readonly property var _parsed: safeParse(vouchersJson)
|
||||
readonly property var vouchers: (_parsed && _parsed.vouchers)
|
||||
? _parsed.vouchers
|
||||
: (Array.isArray(_parsed) ? _parsed : [])
|
||||
readonly property string tip: (_parsed && _parsed.tip) ? String(_parsed.tip) : ""
|
||||
|
||||
function safeParse(s) {
|
||||
try { return s && s.length > 0 ? JSON.parse(s) : null } catch (e) { return null }
|
||||
}
|
||||
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
// ---- Claimable vouchers card ----
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: vouchersCol.implicitHeight + 2 * Theme.spacing.large
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: vouchersCol
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: qsTr("Claimable vouchers")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
// Pending counter badge
|
||||
Rectangle {
|
||||
radius: Theme.spacing.radiusSmall
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
implicitWidth: pendingText.implicitWidth + 2 * Theme.spacing.small
|
||||
implicitHeight: pendingText.implicitHeight + Theme.spacing.tiny
|
||||
LogosText {
|
||||
id: pendingText
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("%1 pending").arg(root.vouchers.length)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosText {
|
||||
visible: root.tip.length > 0
|
||||
text: qsTr("tip %1").arg(root.tip)
|
||||
elide: Text.ElideMiddle
|
||||
Layout.maximumWidth: 140
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
InfoButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: qsTr("Claim block-leader rewards. The list shows your pending claimable vouchers (refreshed each block). Claim submits a claim transaction; the protocol selects which voucher it consumes.")
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontally-sliding list of voucher cards.
|
||||
ListView {
|
||||
id: vouchersList
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 78
|
||||
visible: root.vouchers.length > 0
|
||||
orientation: ListView.Horizontal
|
||||
clip: true
|
||||
spacing: Theme.spacing.small
|
||||
model: root.vouchers
|
||||
snapMode: ListView.SnapToItem
|
||||
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
delegate: Rectangle {
|
||||
width: 260
|
||||
height: ListView.view.height
|
||||
radius: Theme.spacing.radiusSmall
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacing.small
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Voucher %1").arg(index + 1)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
VoucherField {
|
||||
label: qsTr("cm")
|
||||
value: modelData && modelData.commitment ? String(modelData.commitment) : ""
|
||||
}
|
||||
VoucherField {
|
||||
label: qsTr("nf")
|
||||
value: modelData && modelData.nullifier ? String(modelData.nullifier) : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogosText {
|
||||
visible: root.vouchers.length === 0
|
||||
text: qsTr("No claimable vouchers.")
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Claim action ----
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: claimRow.height + 2 * Theme.spacing.large
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
id: claimRow
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacing.large
|
||||
|
||||
LogosButton {
|
||||
id: leaderClaimButton
|
||||
Layout.preferredWidth: 140
|
||||
text: qsTr("Claim")
|
||||
onClicked: root.claimLeaderRewardsRequested()
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
Layout.fillWidth: true
|
||||
enabled: true
|
||||
padding: Theme.spacing.small
|
||||
contentItem: RowLayout {
|
||||
width: parent.width
|
||||
anchors.centerIn: parent
|
||||
LogosText {
|
||||
id: leaderClaimResultText
|
||||
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.copyToClipboard(leaderClaimResultText.text)
|
||||
visible: leaderClaimResultText.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
// A labelled, elided, copyable hash inside a voucher card.
|
||||
component VoucherField: RowLayout {
|
||||
id: vf
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.tiny
|
||||
LogosText {
|
||||
text: vf.label
|
||||
Layout.preferredWidth: 20
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: vf.value || "—"
|
||||
elide: Text.ElideMiddle
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: "monospace"
|
||||
}
|
||||
LogosCopyButton {
|
||||
Layout.preferredHeight: 24
|
||||
Layout.preferredWidth: 24
|
||||
visible: vf.value.length > 0
|
||||
onCopyText: root.copyToClipboard(vf.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
Control {
|
||||
id: root
|
||||
|
||||
// --- Public API ---
|
||||
required property var logModel // ListModel with "text" role
|
||||
|
||||
signal clearRequested()
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.background
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacing.large
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Logs")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
LogosButton {
|
||||
text: qsTr("Clear")
|
||||
Layout.preferredWidth: 80
|
||||
Layout.preferredHeight: 32
|
||||
onClicked: root.clearRequested()
|
||||
}
|
||||
}
|
||||
|
||||
// Log list
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ListView {
|
||||
id: logsListView
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
model: root.logModel
|
||||
spacing: 2
|
||||
|
||||
// Auto-scroll to the latest log on insert. Use the ListView's
|
||||
// own `count` (it's always available and emits countChanged) —
|
||||
// the model replica is a QAbstractItemModelReplica and does
|
||||
// not carry the source-side `count` Q_PROPERTY through QtRO.
|
||||
onCountChanged: if (count > 0) positionViewAtEnd()
|
||||
|
||||
delegate: ItemDelegate{
|
||||
width: ListView.view.width
|
||||
contentItem: LogosText {
|
||||
// The remoted log model can briefly report an undefined
|
||||
// "text" role while the replica syncs — coerce to "".
|
||||
text: model.text || ""
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
background: Rectangle {
|
||||
color: hovered ? Theme.palette.background: "transparent"
|
||||
radius: 2
|
||||
}
|
||||
onClicked: root.copyToClipboard(model.text || "")
|
||||
}
|
||||
|
||||
LogosText {
|
||||
// ListView's `count` reflects the model row count and has
|
||||
// a NOTIFY signal — using it here gives the binding
|
||||
// automatic refresh, unlike `root.logModel.count`.
|
||||
visible: logsListView.count === 0
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("No logs yet...")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/qml/views/NodeInfoView.qml
Normal file
62
src/qml/views/NodeInfoView.qml
Normal file
@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
|
||||
// "Node info" card — node identity that doesn't depend on the consensus
|
||||
// runtime. Currently the self libp2p peer id (derived from the user config).
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string peerId: ""
|
||||
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
implicitHeight: contentCol.implicitHeight + 2 * Theme.spacing.large
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: contentCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Node info")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: qsTr("Peer ID")
|
||||
Layout.preferredWidth: 70
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: root.peerId || qsTr("—")
|
||||
elide: Text.ElideMiddle
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
LogosCopyButton {
|
||||
Layout.preferredHeight: 24
|
||||
Layout.preferredWidth: 24
|
||||
visible: root.peerId.length > 0
|
||||
onCopyText: root.copyToClipboard(root.peerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -117,6 +117,9 @@ Rectangle {
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
// Config can't be changed while the node is running — hide the
|
||||
// button entirely (not just disable it) in that state.
|
||||
visible: !root.isRunning
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: 100
|
||||
Layout.preferredHeight: 40
|
||||
|
||||
241
src/qml/views/TransferView.qml
Normal file
241
src/qml/views/TransferView.qml
Normal file
@ -0,0 +1,241 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
|
||||
// Transfer funds panel. Extracted from the former WalletView.
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property var accountsModel
|
||||
|
||||
signal transferRequested(string fromKeyHex, string toKeyHex, string amount)
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
function setTransferResult(text) {
|
||||
transferResultText.text = text
|
||||
}
|
||||
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
Rectangle {
|
||||
id: transferRect
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: transferCol.height + 2 * Theme.spacing.large
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: transferCol
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
LogosText {
|
||||
text: qsTr("Transfer funds")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
InfoButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: qsTr("Send funds between addresses. Choose a source address (its balance is shown), enter the recipient key and amount, then press Send.")
|
||||
}
|
||||
}
|
||||
|
||||
StyledAddressComboBox {
|
||||
id: transferFromCombo
|
||||
model: root.accountsModel
|
||||
textRole: "address"
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: transferToField
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
placeholderText: qsTr("To key (64 hex chars)")
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: transferAmountField
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
placeholderText: qsTr("Amount")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: transferButton.implicitHeight
|
||||
|
||||
LogosButton {
|
||||
id: transferButton
|
||||
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: true
|
||||
padding: Theme.spacing.small
|
||||
contentItem: RowLayout {
|
||||
width: parent.width
|
||||
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.copyToClipboard(transferResultText.text)
|
||||
visible: transferResultText.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
component StyledAddressComboBox: ComboBox {
|
||||
id: comboControl
|
||||
|
||||
Layout.fillWidth: true
|
||||
padding: Theme.spacing.large
|
||||
editable: true
|
||||
// Balance of the selected row, shown read-only in the closed box (the
|
||||
// editable text holds only the address — it's used as the transfer key).
|
||||
valueRole: "balance"
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
}
|
||||
indicator: LogosText {
|
||||
id: comboIndicator
|
||||
text: "▼"
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
x: comboControl.width - width - Theme.spacing.small
|
||||
y: (comboControl.height - height) / 2
|
||||
visible: comboControl.count > 0
|
||||
}
|
||||
contentItem: Item {
|
||||
implicitWidth: 200
|
||||
implicitHeight: 30
|
||||
|
||||
TextField {
|
||||
id: comboTextField
|
||||
anchors.fill: parent
|
||||
leftPadding: 0
|
||||
rightPadding: (comboControl.count > 0 ? comboIndicator.width + Theme.spacing.small : Theme.spacing.small)
|
||||
+ (balanceLabel.visible ? balanceLabel.width + Theme.spacing.small : 0)
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
text: comboControl.editText
|
||||
onTextChanged: if (text !== comboControl.editText) comboControl.editText = text
|
||||
selectByMouse: true
|
||||
color: Theme.palette.text
|
||||
background: Item { }
|
||||
}
|
||||
LogosText {
|
||||
id: balanceLabel
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: (comboControl.count > 0 ? comboIndicator.width + Theme.spacing.small : 0)
|
||||
+ Theme.spacing.small
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: comboControl.currentIndex >= 0 && text.length > 0
|
||||
text: comboControl.currentValue || ""
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: comboControl.count > 0
|
||||
z: 1
|
||||
onPressed: {
|
||||
comboControl.popup.visible ? comboControl.popup.close() : comboControl.popup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate: ItemDelegate {
|
||||
id: comboDelegate
|
||||
width: comboControl.width
|
||||
contentItem: RowLayout {
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight + Theme.spacing.large
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
text: (typeof model.address !== "undefined" ? model.address : modelData) || ""
|
||||
elide: Text.ElideMiddle
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
LogosText {
|
||||
visible: (typeof model.balance !== "undefined") && (model.balance || "").length > 0
|
||||
text: model.balance || ""
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
horizontalAlignment: Text.AlignRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
background: Rectangle {
|
||||
color: comboDelegate.highlighted ?
|
||||
Theme.palette.backgroundTertiary :
|
||||
Theme.palette.backgroundSecondary
|
||||
}
|
||||
highlighted: comboControl.highlightedIndex === index
|
||||
}
|
||||
popup: Popup {
|
||||
y: comboControl.height - 1
|
||||
width: comboControl.width
|
||||
height: contentItem.implicitHeight
|
||||
padding: 1
|
||||
|
||||
onOpened: if (comboControl.count === 0) close()
|
||||
|
||||
contentItem: ListView {
|
||||
clip: true
|
||||
implicitHeight: contentHeight
|
||||
model: comboControl.popup.visible ? comboControl.delegateModel : null
|
||||
ScrollIndicator.vertical: ScrollIndicator { }
|
||||
highlightFollowsCurrentItem: false
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
radius: Theme.spacing.radiusLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,345 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
import "../controls"
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
required property var accountsModel
|
||||
|
||||
property string lastBalanceError: ""
|
||||
property string lastBalanceErrorAddress: ""
|
||||
|
||||
signal getBalanceRequested(string addressHex)
|
||||
signal refreshAccountsRequested()
|
||||
signal transferRequested(string fromKeyHex, string toKeyHex, string amount)
|
||||
signal claimLeaderRewardsRequested()
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
function setTransferResult(text) {
|
||||
transferResultText.text = text
|
||||
}
|
||||
|
||||
function setLeaderClaimResult(text) {
|
||||
leaderClaimResultText.text = text
|
||||
}
|
||||
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Get balance card
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: actionsCol.height
|
||||
Layout.preferredHeight: Math.min(implicitHeight, 400)
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: balanceCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
LogosText {
|
||||
text: qsTr("Accounts")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosButton {
|
||||
text: qsTr("Refresh")
|
||||
padding: Theme.spacing.small
|
||||
onClicked: root.refreshAccountsRequested()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: balanceListView
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.min(contentHeight, 320)
|
||||
clip: true
|
||||
model: root.accountsModel
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
delegate: AccountDelegate {
|
||||
balanceError: root.lastBalanceErrorAddress === model.address ?
|
||||
root.lastBalanceError: ""
|
||||
onGetBalanceRequested: (addr) => root.getBalanceRequested(addr)
|
||||
onCopyRequested: (text) => root.copyToClipboard(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: actionsCol
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
// Transfer funds card
|
||||
Rectangle {
|
||||
id: transferRect
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: transferCol.height + 2 * Theme.spacing.large
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: transferCol
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Transfer funds")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledAddressComboBox {
|
||||
id: transferFromCombo
|
||||
model: root.accountsModel
|
||||
textRole: "address"
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: transferToField
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
placeholderText: qsTr("To key (64 hex chars)")
|
||||
}
|
||||
|
||||
LogosTextField {
|
||||
id: transferAmountField
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
placeholderText: qsTr("Amount")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: transferButton.implicitHeight
|
||||
|
||||
LogosButton {
|
||||
id: transferButton
|
||||
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: true
|
||||
padding: Theme.spacing.small
|
||||
contentItem: RowLayout {
|
||||
width: parent.width
|
||||
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.copyToClipboard(transferResultText.text)
|
||||
visible: transferResultText.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Leader rewards card
|
||||
Rectangle {
|
||||
id: leaderRewardsRect
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: leaderRewardsCol.height + 2 * Theme.spacing.large
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: leaderRewardsCol
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
text: qsTr("Leader rewards")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
LogosButton {
|
||||
id: leaderClaimButton
|
||||
Layout.preferredWidth: 140
|
||||
text: qsTr("Claim")
|
||||
onClicked: root.claimLeaderRewardsRequested()
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
Layout.fillWidth: true
|
||||
enabled: true
|
||||
padding: Theme.spacing.small
|
||||
contentItem: RowLayout {
|
||||
width: parent.width
|
||||
anchors.centerIn: parent
|
||||
LogosText {
|
||||
id: leaderClaimResultText
|
||||
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.copyToClipboard(leaderClaimResultText.text)
|
||||
visible: leaderClaimResultText.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component StyledAddressComboBox: ComboBox {
|
||||
id: comboControl
|
||||
|
||||
Layout.fillWidth: true
|
||||
padding: Theme.spacing.large
|
||||
editable: true
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
}
|
||||
indicator: LogosText {
|
||||
id: comboIndicator
|
||||
text: "▼"
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
x: comboControl.width - width - Theme.spacing.small
|
||||
y: (comboControl.height - height) / 2
|
||||
visible: comboControl.count > 0
|
||||
}
|
||||
contentItem: Item {
|
||||
implicitWidth: 200
|
||||
implicitHeight: 30
|
||||
|
||||
TextField {
|
||||
id: comboTextField
|
||||
anchors.fill: parent
|
||||
leftPadding: 0
|
||||
rightPadding: comboControl.count > 0 ? comboIndicator.width + Theme.spacing.small : Theme.spacing.small
|
||||
topPadding: 0
|
||||
bottomPadding: 0
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
text: comboControl.editText
|
||||
onTextChanged: if (text !== comboControl.editText) comboControl.editText = text
|
||||
selectByMouse: true
|
||||
color: Theme.palette.text
|
||||
background: Item { }
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: comboControl.count > 0
|
||||
z: 1
|
||||
onPressed: {
|
||||
comboControl.popup.visible ? comboControl.popup.close() : comboControl.popup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate: ItemDelegate {
|
||||
id: comboDelegate
|
||||
width: comboControl.width
|
||||
contentItem: LogosText {
|
||||
width: parent.width
|
||||
height: contentHeight + Theme.spacing.large
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
text: (typeof model.address !== "undefined" ? model.address : modelData) || ""
|
||||
elide: Text.ElideMiddle
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
background: Rectangle {
|
||||
color: comboDelegate.highlighted ?
|
||||
Theme.palette.backgroundTertiary :
|
||||
Theme.palette.backgroundSecondary
|
||||
}
|
||||
highlighted: comboControl.highlightedIndex === index
|
||||
}
|
||||
popup: Popup {
|
||||
y: comboControl.height - 1
|
||||
width: comboControl.width
|
||||
height: contentItem.implicitHeight
|
||||
padding: 1
|
||||
|
||||
onOpened: if (comboControl.count === 0) close()
|
||||
|
||||
contentItem: ListView {
|
||||
clip: true
|
||||
implicitHeight: contentHeight
|
||||
model: comboControl.popup.visible ? comboControl.delegateModel : null
|
||||
ScrollIndicator.vertical: ScrollIndicator { }
|
||||
highlightFollowsCurrentItem: false
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
radius: Theme.spacing.radiusLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
module views
|
||||
StatusConfigView 1.0 StatusConfigView.qml
|
||||
LogsView 1.0 LogsView.qml
|
||||
WalletView 1.0 WalletView.qml
|
||||
BlocksView 1.0 BlocksView.qml
|
||||
CryptarchiaInfoView 1.0 CryptarchiaInfoView.qml
|
||||
NodeInfoView 1.0 NodeInfoView.qml
|
||||
AccountsView 1.0 AccountsView.qml
|
||||
TransferView 1.0 TransferView.qml
|
||||
LeaderRewardsView 1.0 LeaderRewardsView.qml
|
||||
GenerateConfigView 1.0 GenerateConfigView.qml
|
||||
ConfigChoiceView 1.0 ConfigChoiceView.qml
|
||||
SetConfigPathView 1.0 SetConfigPathView.qml
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user