From d738bae390b1e4c879ce2d4b2fd536eaa55a0705 Mon Sep 17 00:00:00 2001 From: jpk68 Date: Wed, 15 Jul 2026 12:43:45 -0400 Subject: [PATCH 1/2] settings: add remote peers table --- components/PeersTable.qml | 148 +++++++++++++++++++++++++++++ pages/settings/SettingsNode.qml | 40 ++++++++ qml.qrc | 1 + src/daemon/PeersModel.cpp | 160 ++++++++++++++++++++++++++++++++ src/daemon/PeersModel.h | 74 +++++++++++++++ src/main/main.cpp | 2 + 6 files changed, 425 insertions(+) create mode 100644 components/PeersTable.qml create mode 100644 src/daemon/PeersModel.cpp create mode 100644 src/daemon/PeersModel.h diff --git a/components/PeersTable.qml b/components/PeersTable.qml new file mode 100644 index 0000000000..c2e8248084 --- /dev/null +++ b/components/PeersTable.qml @@ -0,0 +1,148 @@ +// Copyright (c) 2026, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import QtQuick 2.9 +import QtQuick.Layouts 1.1 + +import "." as MoneroComponents +import "effects/" as MoneroEffects +import "../js/Utils.js" as Utils +import moneroComponents.Clipboard 1.0 + +ColumnLayout { + id: peersTable + spacing: 0 + + property var model + property bool loading: model ? model.loading : false + property int hoveredIndex: -1 + + Clipboard { id: clipboard } + + function formatBytes(bytes) { + if (bytes < 1024) + return bytes + " B"; + if (bytes < 1024 * 1024) + return (bytes / 1024).toFixed(1) + " KB"; + return (bytes / (1024 * 1024)).toFixed(1) + " MB"; + } + + GridLayout { + Layout.fillWidth: true + columns: 8 + columnSpacing: 24 + rowSpacing: 6 + + MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 0; font.bold: true; text: qsTr("Direction") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } + MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 1; Layout.maximumWidth: 220; font.bold: true; text: qsTr("Address") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } + MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 2; font.bold: true; text: qsTr("Network") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } + MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 3; font.bold: true; text: qsTr("Block height") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } + MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 4; font.bold: true; text: qsTr("Connected since") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } + MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 5; font.bold: true; text: qsTr("Time connected") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } + MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 6; font.bold: true; text: qsTr("Sent") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } + MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 7; font.bold: true; text: qsTr("Received") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } + + Rectangle { + Layout.row: 1 + Layout.column: 0 + Layout.columnSpan: 8 + Layout.fillWidth: true + Layout.topMargin: 2 + Layout.bottomMargin: 2 + height: 1 + color: MoneroComponents.Style.dividerColor + opacity: MoneroComponents.Style.dividerOpacity + } + + Repeater { + model: peersTable.model + Rectangle { + Layout.row: index + 2 + Layout.column: 0 + Layout.columnSpan: 8 + Layout.fillWidth: true + Layout.fillHeight: true + color: peersTable.hoveredIndex === index ? MoneroComponents.Style.titleBarButtonHoverColor : "transparent" + + MouseArea { + id: mouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: peersTable.hoveredIndex = index + onExited: if (peersTable.hoveredIndex === index) peersTable.hoveredIndex = -1 + onClicked: { + clipboard.setText(address); + appWindow.showStatusMessage(qsTr("Address copied to clipboard"), 3); + } + } + } + } + + Repeater { + id: peersRepeater + model: peersTable.model + MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 0; text: incoming ? qsTr("Inbound") : qsTr("Outbound"); color: MoneroComponents.Style.defaultFontColor } + } + Repeater { + model: peersTable.model + MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 1; Layout.maximumWidth: 220; text: address; elide: Text.ElideMiddle; color: MoneroComponents.Style.defaultFontColor } + } + Repeater { + model: peersTable.model + MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 2; text: addressType; color: MoneroComponents.Style.defaultFontColor } + } + Repeater { + model: peersTable.model + MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 3; text: blockHeight; color: MoneroComponents.Style.defaultFontColor } + } + Repeater { + model: peersTable.model + MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 4; text: Qt.formatDateTime(new Date((Date.now() / 1000 - liveTime) * 1000), "yyyy-MM-dd hh:mm"); color: MoneroComponents.Style.defaultFontColor } + } + Repeater { + model: peersTable.model + MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 5; text: Utils.ago(Date.now() / 1000 - liveTime); color: MoneroComponents.Style.defaultFontColor } + } + Repeater { + model: peersTable.model + MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 6; text: peersTable.formatBytes(sendCount); color: MoneroComponents.Style.defaultFontColor } + } + Repeater { + model: peersTable.model + MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 7; text: peersTable.formatBytes(recvCount); color: MoneroComponents.Style.defaultFontColor } + } + } + + MoneroComponents.TextPlain { + Layout.topMargin: 8 + color: MoneroComponents.Style.dimmedFontColor + visible: peersRepeater.count === 0 && !peersTable.loading + text: qsTr("No connected peers") + translationManager.emptyString + } +} diff --git a/pages/settings/SettingsNode.qml b/pages/settings/SettingsNode.qml index 28480419ff..f51e3bffab 100644 --- a/pages/settings/SettingsNode.qml +++ b/pages/settings/SettingsNode.qml @@ -33,12 +33,38 @@ import FontAwesome 1.0 import "../../components" as MoneroComponents import "../../components/effects" as MoneroEffects +import moneroComponents.PeersModel 1.0 Rectangle{ color: "transparent" Layout.fillWidth: true property alias nodeHeight: root.height + property bool nodePageActive: false + + function onPageCompleted() { + nodePageActive = true; + if (appWindow.daemonRunning) { + peersModel.refresh(appWindow.currentDaemonAddress); + } + } + + function onPageClosed() { + nodePageActive = false; + } + + PeersModel { + id: peersModel + } + + Timer { + id: peersRefreshTimer + interval: 5000 + repeat: true + running: nodePageActive && appWindow.daemonRunning + onTriggered: peersModel.refresh(appWindow.currentDaemonAddress) + } + /* main layout */ ColumnLayout { id: root @@ -352,6 +378,20 @@ Rectangle{ } } } + + MoneroComponents.TextPlain { + Layout.topMargin: 20 + font.pixelSize: 18 + color: MoneroComponents.Style.defaultFontColor + visible: appWindow.daemonRunning + text: qsTr("Connected peers") + translationManager.emptyString + } + + MoneroComponents.PeersTable { + Layout.fillWidth: true + visible: appWindow.daemonRunning + model: peersModel + } } } } diff --git a/qml.qrc b/qml.qrc index 9223423dc8..01e049d05b 100644 --- a/qml.qrc +++ b/qml.qrc @@ -10,6 +10,7 @@ components/NavbarItem.qml components/RemoteNodeDialog.qml components/RemoteNodeList.qml + components/PeersTable.qml components/SettingsListItem.qml components/Slider.qml components/Tooltip.qml diff --git a/src/daemon/PeersModel.cpp b/src/daemon/PeersModel.cpp new file mode 100644 index 0000000000..19381596ce --- /dev/null +++ b/src/daemon/PeersModel.cpp @@ -0,0 +1,160 @@ +// Copyright (c) 2026, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "PeersModel.h" + +#include +#include + +#include "net/http_client.h" +#include "net/enums.h" +#include "rpc/core_rpc_server_commands_defs.h" +#include "storages/http_abstract_invoke.h" + +namespace { + +QString addressTypeToString(uint8_t addressType) +{ + switch (static_cast(addressType)) + { + case epee::net_utils::address_type::ipv4: + case epee::net_utils::address_type::ipv6: + return QStringLiteral("Clearnet"); + case epee::net_utils::address_type::i2p: + return QStringLiteral("I2P"); + case epee::net_utils::address_type::tor: + return QStringLiteral("Tor"); + default: + return QStringLiteral("Unknown"); + } +} + +} // namespace + +PeersModel::PeersModel(QObject *parent) + : QAbstractListModel(parent) + , m_scheduler(this) +{ +} + +PeersModel::~PeersModel() +{ + m_scheduler.shutdownWaitForFinished(); +} + +bool PeersModel::loading() const +{ + return m_loading; +} + +void PeersModel::refresh(const QString &daemonAddress) +{ + if (m_loading) + return; + + m_loading = true; + emit loadingChanged(); + + m_scheduler.run([this, daemonAddress] { + QVariantList peers; + + const int splitIndex = daemonAddress.lastIndexOf(':'); + const QString host = splitIndex >= 0 ? daemonAddress.left(splitIndex) : daemonAddress; + const QString port = splitIndex >= 0 ? daemonAddress.mid(splitIndex + 1) : QString(); + + epee::net_utils::http::http_simple_client httpClient; + httpClient.set_server(host.toStdString(), port.toStdString(), {}); + + cryptonote::COMMAND_RPC_GET_CONNECTIONS::request req = AUTO_VAL_INIT(req); + cryptonote::COMMAND_RPC_GET_CONNECTIONS::response res = AUTO_VAL_INIT(res); + + bool ok = epee::net_utils::invoke_http_json_rpc("/json_rpc", "get_connections", req, res, httpClient, std::chrono::seconds(3)); + if (ok) + { + for (const cryptonote::connection_info &conn : res.connections) + { + QVariantMap peer; + peer.insert(QStringLiteral("incoming"), conn.incoming); + peer.insert(QStringLiteral("address"), QString::fromStdString(conn.address)); + peer.insert(QStringLiteral("addressType"), addressTypeToString(conn.address_type)); + peer.insert(QStringLiteral("blockHeight"), static_cast(conn.height)); + peer.insert(QStringLiteral("liveTime"), static_cast(conn.live_time)); + peer.insert(QStringLiteral("recvCount"), static_cast(conn.recv_count)); + peer.insert(QStringLiteral("sendCount"), static_cast(conn.send_count)); + peers.append(peer); + } + } + else + { + qDebug() << "PeersModel: get_connections RPC call failed for" << daemonAddress; + } + + QMetaObject::invokeMethod(this, "onConnectionsFetched", Qt::QueuedConnection, Q_ARG(QVariantList, peers)); + }); +} + +void PeersModel::onConnectionsFetched(const QVariantList &peers) +{ + beginResetModel(); + m_peers = peers; + endResetModel(); + + m_loading = false; + emit loadingChanged(); +} + +QVariant PeersModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= m_peers.size()) + return QVariant(); + + const QString key = QString::fromLatin1(roleNames().value(role)); + if (key.isEmpty()) + return QVariant(); + + return m_peers.at(index.row()).toMap().value(key); +} + +int PeersModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_peers.size(); +} + +QHash PeersModel::roleNames() const +{ + QHash roles = QAbstractListModel::roleNames(); + roles.insert(IncomingRole, "incoming"); + roles.insert(AddressRole, "address"); + roles.insert(AddressTypeRole, "addressType"); + roles.insert(HeightRole, "blockHeight"); + roles.insert(LiveTimeRole, "liveTime"); + roles.insert(RecvCountRole, "recvCount"); + roles.insert(SendCountRole, "sendCount"); + return roles; +} diff --git a/src/daemon/PeersModel.h b/src/daemon/PeersModel.h new file mode 100644 index 0000000000..32f56b2618 --- /dev/null +++ b/src/daemon/PeersModel.h @@ -0,0 +1,74 @@ +// Copyright (c) 2026, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include + +#include "qt/FutureScheduler.h" + +class PeersModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) + +public: + enum PeerRole { + IncomingRole = Qt::UserRole + 1, + AddressRole, + AddressTypeRole, + HeightRole, + LiveTimeRole, + RecvCountRole, + SendCountRole + }; + Q_ENUM(PeerRole) + + explicit PeersModel(QObject *parent = nullptr); + ~PeersModel(); + + Q_INVOKABLE void refresh(const QString &daemonAddress); + + bool loading() const; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + +signals: + void loadingChanged(); + +private slots: + void onConnectionsFetched(const QVariantList &peers); + +private: + QVariantList m_peers; + bool m_loading = false; + mutable FutureScheduler m_scheduler; +}; diff --git a/src/main/main.cpp b/src/main/main.cpp index 21993eaf57..13c61e886b 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -79,6 +79,7 @@ // IOS exclusions #ifndef Q_OS_IOS #include "daemon/DaemonManager.h" +#include "daemon/PeersModel.h" #include "p2pool/P2PoolManager.h" #endif @@ -416,6 +417,7 @@ Verify update binary using 'shasum'-compatible (SHA256 algo) output signed by tw "DaemonManager can't be instantiated directly"); qmlRegisterUncreatableType("moneroComponents.P2PoolManager", 1, 0, "P2PoolManager", "P2PoolManager can't be instantiated directly"); + qmlRegisterType("moneroComponents.PeersModel", 1, 0, "PeersModel"); #endif qmlRegisterUncreatableType("moneroComponents.AddressBookModel", 1, 0, "AddressBookModel", "AddressBookModel can't be instantiated directly"); From 593feb90a94b1d334c0f4edbe0f729456e0f8c65 Mon Sep 17 00:00:00 2001 From: jpk68 Date: Wed, 15 Jul 2026 12:50:56 -0400 Subject: [PATCH 2/2] formatting fixes --- components/PeersTable.qml | 134 +++++++++++++++++++++++++++++++++----- 1 file changed, 117 insertions(+), 17 deletions(-) diff --git a/components/PeersTable.qml b/components/PeersTable.qml index c2e8248084..a677914045 100644 --- a/components/PeersTable.qml +++ b/components/PeersTable.qml @@ -42,7 +42,9 @@ ColumnLayout { property bool loading: model ? model.loading : false property int hoveredIndex: -1 - Clipboard { id: clipboard } + Clipboard { + id: clipboard + } function formatBytes(bytes) { if (bytes < 1024) @@ -58,14 +60,70 @@ ColumnLayout { columnSpacing: 24 rowSpacing: 6 - MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 0; font.bold: true; text: qsTr("Direction") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } - MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 1; Layout.maximumWidth: 220; font.bold: true; text: qsTr("Address") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } - MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 2; font.bold: true; text: qsTr("Network") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } - MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 3; font.bold: true; text: qsTr("Block height") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } - MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 4; font.bold: true; text: qsTr("Connected since") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } - MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 5; font.bold: true; text: qsTr("Time connected") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } - MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 6; font.bold: true; text: qsTr("Sent") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } - MoneroComponents.TextPlain { Layout.row: 0; Layout.column: 7; font.bold: true; text: qsTr("Received") + translationManager.emptyString; color: MoneroComponents.Style.dimmedFontColor } + MoneroComponents.TextPlain { + Layout.row: 0 + Layout.column: 0 + font.bold: true + text: qsTr("Direction") + translationManager.emptyString + color: MoneroComponents.Style.dimmedFontColor + } + + MoneroComponents.TextPlain { + Layout.row: 0 + Layout.column: 1 + Layout.maximumWidth: 220 + font.bold: true + text: qsTr("Address") + translationManager.emptyString + color: MoneroComponents.Style.dimmedFontColor + } + + MoneroComponents.TextPlain { + Layout.row: 0 + Layout.column: 2 + font.bold: true + text: qsTr("Network") + translationManager.emptyString + color: MoneroComponents.Style.dimmedFontColor + } + + MoneroComponents.TextPlain { + Layout.row: 0 + Layout.column: 3 + font.bold: true + text: qsTr("Block height") + translationManager.emptyString + color: MoneroComponents.Style.dimmedFontColor + } + + MoneroComponents.TextPlain { + Layout.row: 0 + Layout.column: 4 + font.bold: true + text: qsTr("Connected since") + translationManager.emptyString + color: MoneroComponents.Style.dimmedFontColor + } + + MoneroComponents.TextPlain { + Layout.row: 0 + Layout.column: 5 + font.bold: true + text: qsTr("Time connected") + translationManager.emptyString + color: MoneroComponents.Style.dimmedFontColor + } + + MoneroComponents.TextPlain { + Layout.row: 0 + Layout.column: 6 + font.bold: true + text: qsTr("Sent") + translationManager.emptyString + color: MoneroComponents.Style.dimmedFontColor + } + + MoneroComponents.TextPlain { + Layout.row: 0 + Layout.column: 7 + font.bold: true + text: qsTr("Received") + translationManager.emptyString + color: MoneroComponents.Style.dimmedFontColor + } Rectangle { Layout.row: 1 @@ -107,35 +165,77 @@ ColumnLayout { Repeater { id: peersRepeater model: peersTable.model - MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 0; text: incoming ? qsTr("Inbound") : qsTr("Outbound"); color: MoneroComponents.Style.defaultFontColor } + MoneroComponents.TextPlain { + Layout.row: index + 2 + Layout.column: 0 + text: incoming ? qsTr("Inbound") : qsTr("Outbound") + color: MoneroComponents.Style.defaultFontColor + } } Repeater { model: peersTable.model - MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 1; Layout.maximumWidth: 220; text: address; elide: Text.ElideMiddle; color: MoneroComponents.Style.defaultFontColor } + MoneroComponents.TextPlain { + Layout.row: index + 2 + Layout.column: 1 + Layout.maximumWidth: 220 + text: address + elide: Text.ElideMiddle + color: MoneroComponents.Style.defaultFontColor + } } Repeater { model: peersTable.model - MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 2; text: addressType; color: MoneroComponents.Style.defaultFontColor } + MoneroComponents.TextPlain { + Layout.row: index + 2 + Layout.column: 2 + text: addressType + color: MoneroComponents.Style.defaultFontColor + } } Repeater { model: peersTable.model - MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 3; text: blockHeight; color: MoneroComponents.Style.defaultFontColor } + MoneroComponents.TextPlain { + Layout.row: index + 2 + Layout.column: 3 + text: blockHeight + color: MoneroComponents.Style.defaultFontColor + } } Repeater { model: peersTable.model - MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 4; text: Qt.formatDateTime(new Date((Date.now() / 1000 - liveTime) * 1000), "yyyy-MM-dd hh:mm"); color: MoneroComponents.Style.defaultFontColor } + MoneroComponents.TextPlain { + Layout.row: index + 2 + Layout.column: 4 + text: Qt.formatDateTime(new Date((Date.now() / 1000 - liveTime) * 1000), "yyyy-MM-dd hh:mm") + color: MoneroComponents.Style.defaultFontColor + } } Repeater { model: peersTable.model - MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 5; text: Utils.ago(Date.now() / 1000 - liveTime); color: MoneroComponents.Style.defaultFontColor } + MoneroComponents.TextPlain { + Layout.row: index + 2 + Layout.column: 5 + text: Utils.ago(Date.now() / 1000 - liveTime) + color: MoneroComponents.Style.defaultFontColor + } } Repeater { model: peersTable.model - MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 6; text: peersTable.formatBytes(sendCount); color: MoneroComponents.Style.defaultFontColor } + MoneroComponents.TextPlain { + Layout.row: index + 2 + Layout.column: 6 + text: peersTable.formatBytes(sendCount) + color: MoneroComponents.Style.defaultFontColor + } } Repeater { model: peersTable.model - MoneroComponents.TextPlain { Layout.row: index + 2; Layout.column: 7; text: peersTable.formatBytes(recvCount); color: MoneroComponents.Style.defaultFontColor } + MoneroComponents.TextPlain { + Layout.row: index + 2 + Layout.column: 7 + text: peersTable.formatBytes(recvCount) + color: MoneroComponents.Style.defaultFontColor + } } }