From 901a7fa7d4f3141e5199f627c148d4ebc87c0d09 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 20 Jun 2026 15:59:14 +0200 Subject: [PATCH] Use structure view for blocks events --- CMakeLists.txt | 4 +- src/BlockModel.cpp | 167 ++++++++++++++++++ src/BlockModel.h | 69 ++++++++ src/BlockchainBackend.cpp | 15 +- src/BlockchainBackend.h | 14 +- src/BlockchainBackend.rep | 2 +- src/LogModel.cpp | 43 ----- src/LogModel.h | 26 --- src/qml/BlockchainView.qml | 8 +- src/qml/controls/BlockDelegate.qml | 262 +++++++++++++++++++++++++++++ src/qml/views/BlocksView.qml | 85 ++++++++++ src/qml/views/LogsView.qml | 99 ----------- src/qml/views/qmldir | 2 +- 13 files changed, 603 insertions(+), 193 deletions(-) create mode 100644 src/BlockModel.cpp create mode 100644 src/BlockModel.h delete mode 100644 src/LogModel.cpp delete mode 100644 src/LogModel.h create mode 100644 src/qml/controls/BlockDelegate.qml create mode 100644 src/qml/views/BlocksView.qml delete mode 100644 src/qml/views/LogsView.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 60e6b42..badd769 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/BlockModel.cpp b/src/BlockModel.cpp new file mode 100644 index 0000000..2f7719b --- /dev/null +++ b/src/BlockModel.cpp @@ -0,0 +1,167 @@ +#include "BlockModel.h" + +#include +#include +#include +#include +#include + +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 BlockModel::roleNames() const +{ + QHash 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":""} + // whose value is itself a JSON-encoded block. Tolerate a few shapes: + // { "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(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(); +} diff --git a/src/BlockModel.h b/src/BlockModel.h new file mode 100644 index 0000000..c955f81 --- /dev/null +++ b/src/BlockModel.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include + +// 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 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 m_entries; +}; diff --git a/src/BlockchainBackend.cpp b/src/BlockchainBackend.cpp index 199d095..89d9da2 100644 --- a/src/BlockchainBackend.cpp +++ b/src/BlockchainBackend.cpp @@ -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) diff --git a/src/BlockchainBackend.h b/src/BlockchainBackend.h index 4c5a342..5936c4f 100644 --- a/src/BlockchainBackend.h +++ b/src/BlockchainBackend.h @@ -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 // "/" (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; }; diff --git a/src/BlockchainBackend.rep b/src/BlockchainBackend.rep index ca27996..817ba80 100644 --- a/src/BlockchainBackend.rep +++ b/src/BlockchainBackend.rep @@ -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)) } diff --git a/src/LogModel.cpp b/src/LogModel.cpp deleted file mode 100644 index 35d9281..0000000 --- a/src/LogModel.cpp +++ /dev/null @@ -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 LogModel::roleNames() const -{ - QHash 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(); -} diff --git a/src/LogModel.h b/src/LogModel.h deleted file mode 100644 index 215de63..0000000 --- a/src/LogModel.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include - -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 roleNames() const override; - - Q_INVOKABLE void append(const QString& line); - Q_INVOKABLE void clear(); - -signals: - void countChanged(); - -private: - QStringList m_lines; -}; diff --git a/src/qml/BlockchainView.qml b/src/qml/BlockchainView.qml index 5b738c2..ce2279e 100644 --- a/src/qml/BlockchainView.qml +++ b/src/qml/BlockchainView.qml @@ -38,7 +38,7 @@ Rectangle { // Models live on the C++ backend and are auto-remoted by ui-host as // "/". 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) } diff --git a/src/qml/controls/BlockDelegate.qml b/src/qml/controls/BlockDelegate.qml new file mode 100644 index 0000000..31e0580 --- /dev/null +++ b/src/qml/controls/BlockDelegate.qml @@ -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 + } + } +} diff --git a/src/qml/views/BlocksView.qml b/src/qml/views/BlocksView.qml new file mode 100644 index 0000000..a1795c6 --- /dev/null +++ b/src/qml/views/BlocksView.qml @@ -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 + } + } + } + } +} diff --git a/src/qml/views/LogsView.qml b/src/qml/views/LogsView.qml deleted file mode 100644 index f0a4372..0000000 --- a/src/qml/views/LogsView.qml +++ /dev/null @@ -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 - } - } - } - } -} diff --git a/src/qml/views/qmldir b/src/qml/views/qmldir index bcf9820..34b0946 100644 --- a/src/qml/views/qmldir +++ b/src/qml/views/qmldir @@ -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