mirror of
https://github.com/logos-blockchain/logos-blockchain-ui.git
synced 2026-07-08 00:39:28 +00:00
Use structure view for blocks events
This commit is contained in:
parent
c34ba1552f
commit
901a7fa7d4
@ -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
|
||||
|
||||
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"));
|
||||
@ -423,9 +418,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.
|
||||
@ -57,7 +57,7 @@ public slots:
|
||||
QStringList fundingPublicKeyHexes,
|
||||
QString maxTxFee,
|
||||
QString optionalTipHex) override;
|
||||
void clearLogs() override;
|
||||
void clearBlocks() override;
|
||||
void copyToClipboard(QString text) override;
|
||||
|
||||
private:
|
||||
@ -67,7 +67,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;
|
||||
};
|
||||
|
||||
@ -18,6 +18,6 @@ class BlockchainBackend
|
||||
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;
|
||||
};
|
||||
@ -38,7 +38,7 @@ Rectangle {
|
||||
// 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
|
||||
@ -240,12 +240,12 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
LogsView {
|
||||
BlocksView {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumHeight: 150
|
||||
|
||||
logModel: root.logModel
|
||||
onClearRequested: if (root.backend) root.backend.clearLogs()
|
||||
blockModel: root.blockModel
|
||||
onClearRequested: if (root.backend) root.backend.clearBlocks()
|
||||
onCopyToClipboard: (text) => {
|
||||
root.copyText(text)
|
||||
}
|
||||
|
||||
262
src/qml/controls/BlockDelegate.qml
Normal file
262
src/qml/controls/BlockDelegate.qml
Normal file
@ -0,0 +1,262 @@
|
||||
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 || []
|
||||
|
||||
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: model.parsed
|
||||
text: qsTr("slot %1").arg(model.slot || qsTr("?"))
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
// Version badge
|
||||
Rectangle {
|
||||
visible: model.parsed && (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: !model.parsed
|
||||
text: qsTr("Unparsed block")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.warning
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
LogosText {
|
||||
visible: model.parsed
|
||||
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: model.parsed }
|
||||
HashRow { label: qsTr("Block root"); value: model.blockRoot || ""; visible: model.parsed }
|
||||
HashRow { label: qsTr("Signature"); value: model.signature || ""; visible: model.parsed }
|
||||
|
||||
// Proof of leadership (collapsible sub-group)
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: model.parsed
|
||||
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: model.parsed && del.proofExpanded
|
||||
spacing: Theme.spacing.small
|
||||
HashRow { label: qsTr("Leader key"); value: model.leaderKey || "" }
|
||||
HashRow { label: qsTr("Entropy"); value: model.entropy || "" }
|
||||
HashRow { label: qsTr("Proof"); value: model.proof || "" }
|
||||
HashRow { label: qsTr("Voucher cm"); value: model.voucherCm || "" }
|
||||
}
|
||||
|
||||
// Transactions
|
||||
LogosText {
|
||||
visible: model.parsed
|
||||
text: qsTr("Transactions (%1)").arg(model.txCount || 0)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: model.parsed
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
Repeater {
|
||||
model: del.expanded ? del.transactionsList : []
|
||||
delegate: TxItem {
|
||||
required property int index
|
||||
required property string modelData
|
||||
Layout.fillWidth: true
|
||||
idx: index
|
||||
json: modelData
|
||||
}
|
||||
}
|
||||
|
||||
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: !model.parsed
|
||||
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: !model.parsed
|
||||
json: model.rawJson || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Inline helpers ----
|
||||
|
||||
component HashRow: RowLayout {
|
||||
property string label
|
||||
property string value
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: label
|
||||
Layout.preferredWidth: 110
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: value && value.length > 0 ? value : "—"
|
||||
elide: Text.ElideMiddle
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: "monospace"
|
||||
}
|
||||
LogosCopyButton {
|
||||
visible: value && value.length > 0
|
||||
onCopyText: del.copyToClipboard(value)
|
||||
}
|
||||
}
|
||||
|
||||
component JsonBlock: Rectangle {
|
||||
property string json
|
||||
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: parent.json
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: "monospace"
|
||||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
}
|
||||
|
||||
component TxItem: ColumnLayout {
|
||||
id: txRoot
|
||||
property int idx
|
||||
property string json
|
||||
property bool open: false
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: (txRoot.open ? "▾ " : "▸ ") + qsTr("Transaction %1").arg(txRoot.idx + 1)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
TapHandler { onTapped: txRoot.open = !txRoot.open }
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosCopyButton { onCopyText: del.copyToClipboard(txRoot.json) }
|
||||
}
|
||||
JsonBlock {
|
||||
Layout.fillWidth: true
|
||||
visible: txRoot.open
|
||||
json: txRoot.json
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
module views
|
||||
StatusConfigView 1.0 StatusConfigView.qml
|
||||
LogsView 1.0 LogsView.qml
|
||||
BlocksView 1.0 BlocksView.qml
|
||||
AccountsView 1.0 AccountsView.qml
|
||||
TransferView 1.0 TransferView.qml
|
||||
LeaderRewardsView 1.0 LeaderRewardsView.qml
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user