diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index c32b022ee8..f1b1095dfd 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -36,7 +36,9 @@ QT_FORMS_UI = \ QT_MOC_CPP = \ qml/moc_appmode.cpp \ + qml/moc_chainmodel.cpp \ qml/moc_nodemodel.cpp \ + qml/moc_options_model.cpp \ qt/moc_addressbookpage.cpp \ qt/moc_addresstablemodel.cpp \ qt/moc_askpassphrasedialog.cpp \ @@ -110,8 +112,10 @@ QT_QRC_LOCALE = qt/bitcoin_locale.qrc BITCOIN_QT_H = \ qml/appmode.h \ qml/bitcoin.h \ + qml/chainmodel.h \ qml/imageprovider.h \ qml/nodemodel.h \ + qml/options_model.h \ qml/util.h \ qt/addressbookpage.h \ qt/addresstablemodel.h \ @@ -289,8 +293,10 @@ BITCOIN_QT_WALLET_CPP = \ BITCOIN_QML_BASE_CPP = \ qml/bitcoin.cpp \ + qml/chainmodel.cpp \ qml/imageprovider.cpp \ qml/nodemodel.cpp \ + qml/options_model.cpp \ qml/util.cpp QML_RES_FONTS = \ @@ -305,6 +311,7 @@ QML_RES_ICONS = \ qml/res/icons/blocktime-light.png \ qml/res/icons/caret-left.png \ qml/res/icons/caret-right.png \ + qml/res/icons/check.png \ qml/res/icons/cross.png \ qml/res/icons/export.png \ qml/res/icons/gear.png \ @@ -319,12 +326,15 @@ QML_QRC = qml/bitcoin_qml.qrc QML_RES_QML = \ qml/components/AboutOptions.qml \ qml/components/BlockCounter.qml \ + qml/components/BlockClockComponent.qml \ qml/components/ConnectionOptions.qml \ qml/components/ConnectionSettings.qml \ qml/components/DeveloperOptions.qml \ + qml/components/PeersIndicator.qml \ qml/components/StorageLocations.qml \ qml/components/StorageOptions.qml \ qml/components/StorageSettings.qml \ + qml/controls/BlockClock.qml \ qml/controls/ContinueButton.qml \ qml/controls/ExternalLink.qml \ qml/controls/Header.qml \ @@ -343,6 +353,8 @@ QML_RES_QML = \ qml/controls/ValueInput.qml \ qml/pages/initerrormessage.qml \ qml/pages/main.qml \ + qml/pages/node/NodeRunner.qml \ + qml/pages/node/NodeSettings.qml \ qml/pages/onboarding/OnboardingBlockclock.qml \ qml/pages/onboarding/OnboardingConnection.qml \ qml/pages/onboarding/OnboardingCover.qml \ diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index 7a3d88b18f..25524b8b65 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -128,6 +128,9 @@ class Chain //! Get block hash. Height must be valid or this function will abort. virtual uint256 getBlockHash(int height) = 0; + //! Get block time, Height must be valid or this function will abort. + virtual int64_t getBlockTime(int height) = 0; + //! Check that the block is available on disk (i.e. has not been //! pruned), and contains transactions. virtual bool haveBlockOnDisk(int height) = 0; diff --git a/src/interfaces/node.h b/src/interfaces/node.h index dbdb21eb91..c64e5e249b 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -33,6 +33,7 @@ enum class SynchronizationState; enum class TransactionError; struct CNodeStateStats; struct bilingual_str; +struct PeersNumByType; namespace node { struct NodeContext; } // namespace node @@ -238,7 +239,7 @@ class Node virtual std::unique_ptr handleInitWallet(InitWalletFn fn) = 0; //! Register handler for number of connections changed messages. - using NotifyNumConnectionsChangedFn = std::function; + using NotifyNumConnectionsChangedFn = std::function; virtual std::unique_ptr handleNotifyNumConnectionsChanged(NotifyNumConnectionsChangedFn fn) = 0; //! Register handler for network active messages. diff --git a/src/net.cpp b/src/net.cpp index 374e93a2bd..3b4d829ec2 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1133,15 +1133,47 @@ void CConnman::DisconnectNodes() void CConnman::NotifyNumConnectionsChanged() { - size_t nodes_size; + decltype(m_nodes) nodes_copy; { LOCK(m_nodes_mutex); - nodes_size = m_nodes.size(); + nodes_copy = m_nodes; } - if(nodes_size != nPrevNodeCount) { - nPrevNodeCount = nodes_size; + + int num_outbound_full_relay{0}; + int num_block_relay{0}; + int num_manual{0}; + int num_inbound{0}; + for (const auto* node : nodes_copy) { + switch (node->m_conn_type) { + case ConnectionType::OUTBOUND_FULL_RELAY: + ++num_outbound_full_relay; + break; + case ConnectionType::BLOCK_RELAY: + ++num_block_relay; + break; + case ConnectionType::MANUAL: + ++num_manual; + break; + case ConnectionType::INBOUND: + ++num_inbound; + break; + case ConnectionType::FEELER: + case ConnectionType::ADDR_FETCH: + break; + } + } + + if (num_outbound_full_relay != m_num_outbound_full_relay || + num_block_relay != m_num_block_relay || + num_manual != m_num_manual || + num_inbound != m_num_inbound) { + m_num_outbound_full_relay = num_outbound_full_relay; + m_num_block_relay = num_block_relay; + m_num_manual = num_manual; + m_num_inbound = num_inbound; if (m_client_interface) { - m_client_interface->NotifyNumConnectionsChanged(nodes_size); + m_client_interface->NotifyNumConnectionsChanged( + {num_outbound_full_relay, num_block_relay, num_manual, num_inbound, static_cast(nodes_copy.size())}); } } } diff --git a/src/net.h b/src/net.h index 44641fb47c..162018992a 100644 --- a/src/net.h +++ b/src/net.h @@ -46,6 +46,7 @@ class BanMan; class CNode; class CScheduler; struct bilingual_str; +struct PeersNumByType; /** Default for -whitelistrelay. */ static const bool DEFAULT_WHITELISTRELAY = true; @@ -1010,7 +1011,11 @@ class CConnman std::list m_nodes_disconnected; mutable RecursiveMutex m_nodes_mutex; std::atomic nLastNodeId{0}; - unsigned int nPrevNodeCount{0}; + int m_num_outbound_full_relay{0}; + int m_num_block_relay{0}; + int m_num_manual{0}; + int m_num_inbound{0}; + /** * Cache responses to addr requests to minimize privacy leak. diff --git a/src/node/interface_ui.cpp b/src/node/interface_ui.cpp index fa90d6fda7..836455b25f 100644 --- a/src/node/interface_ui.cpp +++ b/src/node/interface_ui.cpp @@ -48,7 +48,7 @@ bool CClientUIInterface::ThreadSafeMessageBox(const bilingual_str& message, cons bool CClientUIInterface::ThreadSafeQuestion(const bilingual_str& message, const std::string& non_interactive_message, const std::string& caption, unsigned int style) { return g_ui_signals.ThreadSafeQuestion(message, non_interactive_message, caption, style).value_or(false);} void CClientUIInterface::InitMessage(const std::string& message) { return g_ui_signals.InitMessage(message); } void CClientUIInterface::InitWallet() { return g_ui_signals.InitWallet(); } -void CClientUIInterface::NotifyNumConnectionsChanged(int newNumConnections) { return g_ui_signals.NotifyNumConnectionsChanged(newNumConnections); } +void CClientUIInterface::NotifyNumConnectionsChanged(PeersNumByType newNumConnections) { return g_ui_signals.NotifyNumConnectionsChanged(newNumConnections); } void CClientUIInterface::NotifyNetworkActiveChanged(bool networkActive) { return g_ui_signals.NotifyNetworkActiveChanged(networkActive); } void CClientUIInterface::NotifyAlertChanged() { return g_ui_signals.NotifyAlertChanged(); } void CClientUIInterface::ShowProgress(const std::string& title, int nProgress, bool resume_possible) { return g_ui_signals.ShowProgress(title, nProgress, resume_possible); } diff --git a/src/node/interface_ui.h b/src/node/interface_ui.h index 316d75167e..c11562be61 100644 --- a/src/node/interface_ui.h +++ b/src/node/interface_ui.h @@ -20,6 +20,14 @@ class connection; } } // namespace boost +struct PeersNumByType { + int outbound_full_relay{0}; + int block_relay{0}; + int manual{0}; + int inbound{0}; + int total{0}; +}; + /** Signals for UI communication. */ class CClientUIInterface { @@ -85,7 +93,7 @@ class CClientUIInterface ADD_SIGNALS_DECL_WRAPPER(InitWallet, void, ); /** Number of network connections changed. */ - ADD_SIGNALS_DECL_WRAPPER(NotifyNumConnectionsChanged, void, int newNumConnections); + ADD_SIGNALS_DECL_WRAPPER(NotifyNumConnectionsChanged, void, PeersNumByType newNumConnections); /** Network activity state changed. */ ADD_SIGNALS_DECL_WRAPPER(NotifyNetworkActiveChanged, void, bool networkActive); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 7a15f3649b..3d461ac9e0 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -514,6 +514,11 @@ class ChainImpl : public Chain LOCK(::cs_main); return Assert(chainman().ActiveChain()[height])->GetBlockHash(); } + int64_t getBlockTime(int height) override + { + LOCK(::cs_main); + return Assert(chainman().ActiveChain()[height])->GetBlockTime(); + } bool haveBlockOnDisk(int height) override { LOCK(::cs_main); diff --git a/src/qml/bitcoin.cpp b/src/qml/bitcoin.cpp index 489171d3ed..c2c1f0c115 100644 --- a/src/qml/bitcoin.cpp +++ b/src/qml/bitcoin.cpp @@ -6,14 +6,17 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include +#include #include #include #include @@ -154,6 +157,7 @@ int QmlGuiMain(int argc, char* argv[]) GUIUtil::LogQtInfo(); std::unique_ptr node = init->makeNode(); + std::unique_ptr chain = init->makeChain(); if (!node->baseInitialize()) { // A dialog with detailed error will have been shown by InitError(). return EXIT_FAILURE; @@ -185,6 +189,15 @@ int QmlGuiMain(int argc, char* argv[]) engine.rootContext()->setContextProperty("nodeModel", &node_model); + OptionsQmlModel options_model{*node}; + engine.rootContext()->setContextProperty("options", &options_model); + + ChainModel chain_model{*chain}; + engine.rootContext()->setContextProperty("chainModel", &chain_model); + + QObject::connect(&node_model, &NodeModel::setTimeRatioList, &chain_model, &ChainModel::setTimeRatioList); + QObject::connect(&node_model, &NodeModel::setTimeRatioListInitial, &chain_model, &ChainModel::setTimeRatioListInitial); + #ifdef __ANDROID__ AppMode app_mode(AppMode::MOBILE); #else diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index 28e716d544..e0748d13e1 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -1,13 +1,16 @@ components/AboutOptions.qml + components/BlockClockComponent.qml components/BlockCounter.qml components/ConnectionOptions.qml components/ConnectionSettings.qml components/DeveloperOptions.qml + components/PeersIndicator.qml components/StorageLocations.qml components/StorageOptions.qml components/StorageSettings.qml + controls/BlockClock.qml controls/ContinueButton.qml controls/ExternalLink.qml controls/Header.qml @@ -26,6 +29,8 @@ controls/ValueInput.qml pages/initerrormessage.qml pages/main.qml + pages/node/NodeRunner.qml + pages/node/NodeSettings.qml pages/onboarding/OnboardingBlockclock.qml pages/onboarding/OnboardingConnection.qml pages/onboarding/OnboardingCover.qml @@ -46,6 +51,7 @@ ../qt/res/icons/bitcoin.png res/icons/caret-left.png res/icons/caret-right.png + res/icons/check.png res/icons/cross.png res/icons/export.png res/icons/gear.png diff --git a/src/qml/chainmodel.cpp b/src/qml/chainmodel.cpp new file mode 100644 index 0000000000..343cd5a21b --- /dev/null +++ b/src/qml/chainmodel.cpp @@ -0,0 +1,75 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include + +ChainModel::ChainModel(interfaces::Chain& chain) + : m_chain{chain} +{ + timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, &ChainModel::setCurrentTimeRatio); + timer->start(1000); + + QThread* timer_thread = new QThread; + timer->moveToThread(timer_thread); + timer_thread->start(); +} + +void ChainModel::setTimeRatioList(int new_time) +{ + int timeAtMeridian = timestampAtMeridian(); + + if (new_time < timeAtMeridian) { + return; + } + + m_time_ratio_list.push_back(double(new_time - timeAtMeridian) / SECS_IN_12_HOURS); + + Q_EMIT timeRatioListChanged(); +} + +int ChainModel::timestampAtMeridian() +{ + int secsSinceMeridian = (QTime::currentTime().msecsSinceStartOfDay() / 1000) % SECS_IN_12_HOURS; + int currentTimestamp = QDateTime::currentSecsSinceEpoch(); + + return currentTimestamp - secsSinceMeridian; +} + +void ChainModel::setTimeRatioListInitial() +{ + int timeAtMeridian = timestampAtMeridian(); + int first_block_height; + int active_chain_height = m_chain.getHeight().value(); + bool success = m_chain.findFirstBlockWithTimeAndHeight(/*min_time=*/timeAtMeridian, /*min_height=*/0, interfaces::FoundBlock().height(first_block_height)); + + if (!success) { + return; + } + + m_time_ratio_list.clear(); + m_time_ratio_list.push_back(double(QDateTime::currentSecsSinceEpoch() - timeAtMeridian) / SECS_IN_12_HOURS); + + for (int height = first_block_height; height < active_chain_height + 1; height++) { + m_time_ratio_list.push_back(double(m_chain.getBlockTime(height) - timeAtMeridian) / SECS_IN_12_HOURS); + } + + Q_EMIT timeRatioListChanged(); +} + +void ChainModel::setCurrentTimeRatio() +{ + int secsSinceMeridian = (QTime::currentTime().msecsSinceStartOfDay() / 1000) % SECS_IN_12_HOURS; + double currentTimeRatio = double(secsSinceMeridian) / SECS_IN_12_HOURS; + + if (currentTimeRatio < m_time_ratio_list[0].toDouble()) { // That means time has crossed a meridian + m_time_ratio_list.erase(m_time_ratio_list.begin() + 1, m_time_ratio_list.end()); + } + m_time_ratio_list[0] = currentTimeRatio; +} \ No newline at end of file diff --git a/src/qml/chainmodel.h b/src/qml/chainmodel.h new file mode 100644 index 0000000000..2943c9c160 --- /dev/null +++ b/src/qml/chainmodel.h @@ -0,0 +1,50 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QML_CHAINMODEL_H +#define BITCOIN_QML_CHAINMODEL_H + +#include + +#include +#include +#include + +namespace interfaces { +class FoundBlock; +class Chain; +} // namespace interfaces + +static const int SECS_IN_12_HOURS = 43200; + +class ChainModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(QVariantList timeRatioList READ timeRatioList NOTIFY timeRatioListChanged) + +public: + explicit ChainModel(interfaces::Chain& chain); + + QVariantList timeRatioList() const { return m_time_ratio_list; }; + + int timestampAtMeridian(); + + void setCurrentTimeRatio(); + +public Q_SLOTS: + void setTimeRatioList(int new_time); + void setTimeRatioListInitial(); + +Q_SIGNALS: + void timeRatioListChanged(); + +private: + QVariantList m_time_ratio_list{0.0}; + + QTimer* timer; + + interfaces::Chain& m_chain; +}; + +#endif // BITCOIN_QML_CHAINMODEL_H diff --git a/src/qml/components/AboutOptions.qml b/src/qml/components/AboutOptions.qml index 5408d68e51..ec512bf9b2 100644 --- a/src/qml/components/AboutOptions.qml +++ b/src/qml/components/AboutOptions.qml @@ -56,7 +56,7 @@ ColumnLayout { icon.width: 18 background: null onClicked: { - introductions.incrementCurrentIndex() + aboutSwipe.incrementCurrentIndex() } } } diff --git a/src/qml/components/BlockClockComponent.qml b/src/qml/components/BlockClockComponent.qml new file mode 100644 index 0000000000..00ed59e4b5 --- /dev/null +++ b/src/qml/components/BlockClockComponent.qml @@ -0,0 +1,61 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "../controls" + +BlockClock { + id: blockClock + anchors.centerIn: parent + synced: nodeModel.verificationProgress > 0.999 + pause: nodeModel.pause + conns: nodeModel.numOutboundPeers > 0 + + states: [ + State { + name: "intialBlockDownload"; when: !synced && !pause && conns + PropertyChanges { + target: blockClock + + ringProgress: nodeModel.verificationProgress + header: Math.round(ringProgress * 100) + "%" + subText: Math.round(nodeModel.remainingSyncTime/60000) > 0 ? Math.round(nodeModel.remainingSyncTime/60000) + "mins" : Math.round(nodeModel.remainingSyncTime/1000) + "secs" + } + }, + + State { + name: "blockClock"; when: synced && !pause && conns + PropertyChanges { + target: blockClock + + ringProgress: blockList[0] + header: nodeModel.blockTipHeight + subText: "Latest Block" + blockList: chainModel.timeRatioList + } + }, + + State { + name: "Manual Pause"; when: pause + PropertyChanges { + target: blockClock + + ringProgress: 0 + header: "Paused" + subText: "Tap to start" + blockList: {} + } + }, + + State { + name: "Connecting"; when: !pause && !conns + PropertyChanges { + target: blockClock + + ringProgress: 0 + header: "Connecting" + subText: "Please Wait" + blockList: {} + } + } + ] +} diff --git a/src/qml/components/ConnectionOptions.qml b/src/qml/components/ConnectionOptions.qml index be016fe8dc..e0c4079d7e 100644 --- a/src/qml/components/ConnectionOptions.qml +++ b/src/qml/components/ConnectionOptions.qml @@ -31,13 +31,5 @@ ColumnLayout { ButtonGroup.group: group text: qsTr("Only when on Wi-Fi") description: qsTr("Loads quickly when on wi-fi and pauses when on cellular data.") - detail: ProgressIndicator { - implicitWidth: 50 - SequentialAnimation on progress { - loops: Animation.Infinite - SmoothedAnimation { to: 1; velocity: 1; } - SmoothedAnimation { to: 0; velocity: 1; } - } - } } } diff --git a/src/qml/components/PeersIndicator.qml b/src/qml/components/PeersIndicator.qml new file mode 100644 index 0000000000..2aafbb1851 --- /dev/null +++ b/src/qml/components/PeersIndicator.qml @@ -0,0 +1,41 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +// The PeersIndicator component. +// See reference design: https://bitcoindesign.github.io/Bitcoin-Core-App/assets/images/block-clock-connection-states-big.png + +import QtQuick 2.15 +import "../controls" + +Item { + id: root + required property int numOutboundPeers + required property int maxNumOutboundPeers + + implicitWidth: dots.childrenRect.width + implicitHeight: dots.childrenRect.height + + Row { + id: dots + property int size: 5 + spacing: 5 + Repeater { + model: 5 + Rectangle { + width: 3 + height: 3 + radius: width / 2 + color: Theme.color.neutral9 + opacity: (index === 0 && root.numOutboundPeers > 0) || (index + 1 <= dots.size * root.numOutboundPeers / root.maxNumOutboundPeers) ? 0.95 : 0.45 + Behavior on opacity { OpacityAnimator { duration: 100 } } + SequentialAnimation on opacity { + loops: Animation.Infinite + running: numOutboundPeers === 0 && index === 0 + SmoothedAnimation { to: 0; velocity: 2.2 } + SmoothedAnimation { to: 1; velocity: 2.2 } + } + } + } + } +} diff --git a/src/qml/components/StorageLocations.qml b/src/qml/components/StorageLocations.qml index fde4e0a8af..241477e775 100644 --- a/src/qml/components/StorageLocations.qml +++ b/src/qml/components/StorageLocations.qml @@ -15,15 +15,15 @@ ColumnLayout { OptionButton { Layout.fillWidth: true ButtonGroup.group: group - text: qsTr("Default directory") - description: qsTr("The downloaded block data will be saved to the default data directory for your OS.") + text: qsTr("Default") + description: qsTr("Your application directory.") recommended: true checked: true } OptionButton { Layout.fillWidth: true ButtonGroup.group: group - text: qsTr("Custom directory") - description: qsTr("The downloaded block data will be saved to the chosen directory.") + text: qsTr("Custom") + description: qsTr("Choose the directory and storage device.") } } diff --git a/src/qml/components/StorageOptions.qml b/src/qml/components/StorageOptions.qml index f25a892368..01f0bd2419 100644 --- a/src/qml/components/StorageOptions.qml +++ b/src/qml/components/StorageOptions.qml @@ -16,22 +16,21 @@ ColumnLayout { Layout.fillWidth: true ButtonGroup.group: group text: qsTr("Reduce storage") - description: qsTr("Uses about 2GB.") + description: qsTr("Uses about 2GB. For simple wallet use.") recommended: true checked: true - detail: ProgressIndicator { - implicitWidth: 75 - progress: 0.25 + onClicked: { + options.prune = true + options.pruneSizeGB = 2 } } OptionButton { Layout.fillWidth: true ButtonGroup.group: group - text: qsTr("Default") - description: qsTr("Uses about 423GB.") - detail: ProgressIndicator { - implicitWidth: 75 - progress: 0.8 + text: qsTr("Store all data") + description: qsTr("Uses about 550GB. Support the network.") + onClicked: { + options.prune = false } } } diff --git a/src/qml/components/StorageSettings.qml b/src/qml/components/StorageSettings.qml index 90c16f38dd..b4b3263231 100644 --- a/src/qml/components/StorageSettings.qml +++ b/src/qml/components/StorageSettings.qml @@ -11,21 +11,18 @@ ColumnLayout { spacing: 20 Setting { Layout.fillWidth: true - header: qsTr("Store Recent blocks only") - actionItem: OptionSwitch {} - } - Setting { - Layout.fillWidth: true - header: qsTr("Storage limit") - actionItem: ValueInput { - description: qsTr("2 GB") + header: qsTr("Store recent blocks only") + actionItem: OptionSwitch { + checked: options.prune + onToggled: options.prune = checked } } Setting { Layout.fillWidth: true - header: qsTr("Data location") + header: qsTr("Storage limit (GB)") actionItem: ValueInput { - description: qsTr("c://.../data") + description: options.pruneSizeGB + onEditingFinished: options.pruneSizeGB = parseInt(text) } } } diff --git a/src/qml/controls/BlockClock.qml b/src/qml/controls/BlockClock.qml new file mode 100644 index 0000000000..24a3f7b852 --- /dev/null +++ b/src/qml/controls/BlockClock.qml @@ -0,0 +1,157 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import "../components" + +Item { + id: root + + Layout.alignment: Qt.AlignCenter + width: size + height: size + + property real ringProgress: 0 + property string header + property string subText + property bool synced + property bool pause + property bool conns + + property int size: 200 + property real arcBegin: 0 + property real arcEnd: ringProgress * 360 + property real lineWidth: 4 + property string colorCircle: "#f1d54a" + property string colorBackground: Theme.color.neutral2 + + property variant blockList: [] + property variant colorList: ["#EC5445", "#ED6E46", "#EE8847", "#EFA148", "#F0BB49", "#F1D54A"] + + property alias beginAnimation: animationArcBegin.enabled + property alias endAnimation: animationArcEnd.enabled + + property int animationDuration: 250 + + onArcBeginChanged: canvas.requestPaint() + onArcEndChanged: canvas.requestPaint() + onSyncedChanged: canvas.requestPaint() + onBlockListChanged: canvas.requestPaint() + + Behavior on arcBegin { + id: animationArcBegin + enabled: true + NumberAnimation { + duration: root.animationDuration + easing.type: Easing.InOutCubic + } + } + + Behavior on arcEnd { + id: animationArcEnd + enabled: true + NumberAnimation { + easing.type: Easing.Bezier + easing.bezierCurve: [0.5, 0.0, 0.2, 1, 1, 1] + duration: root.animationDuration + } + } + + Canvas { + id: canvas + anchors.fill: parent + rotation: -90 + + onPaint: { + var ctx = getContext("2d") + var x = width / 2 + var y = height / 2 + + ctx.reset() + + // Paint background + ctx.beginPath(); + ctx.arc(x, y, (width / 2) - parent.lineWidth / 2, 0, Math.PI * 2, false) + ctx.lineWidth = root.lineWidth + ctx.strokeStyle = root.colorBackground + ctx.stroke() + + if (!synced) { + var start = Math.PI * (parent.arcBegin / 180) + var end = Math.PI * (parent.arcEnd / 180) + // Paint foreground arc + ctx.beginPath(); + ctx.arc(x, y, (width / 2) - parent.lineWidth / 2, start, end, false) + ctx.lineWidth = root.lineWidth + ctx.strokeStyle = root.colorCircle + ctx.stroke() + } + + else { + var del = 0.0025 + // Paint Block time points + for (var i=1; i 5 ? colorList[5] : colorList[conf] + ctx.stroke() + + // Paint dark segments + var start = Math.PI * ((parent.blockList[i + 1] - del) * 360 / 180) + ctx.beginPath(); + ctx.arc(x, y, (width / 2) - parent.lineWidth/2, start, ends, false) + ctx.lineWidth = 4 + ctx.strokeStyle = root.colorBackground + ctx.stroke(); + } + + // Print last segment + var starts = Math.PI * ((parent.blockList[parent.blockList.length - 1]) * 360 / 180) + var ends = Math.PI * (ringProgress * 360 / 180) + + ctx.beginPath(); + ctx.arc(x, y, (width / 2) - parent.lineWidth / 2, starts, ends, false) + ctx.lineWidth = root.lineWidth + ctx.strokeStyle = colorList[0]; + ctx.stroke() + } + } + } + + ColumnLayout { + anchors.centerIn: root + Image { + Layout.alignment: Qt.AlignCenter + source: "image://images/app" + sourceSize.width: 30 + sourceSize.height: 30 + Layout.bottomMargin: 15 + } + Header { + Layout.fillWidth: true + header: root.header + headerSize: 32 + bold: true + description: root.subText + descriptionMargin: 0 + descriptionColor: Theme.color.neutral4 + Layout.bottomMargin: 20 + } + PeersIndicator { + Layout.alignment: Qt.AlignCenter + numOutboundPeers: nodeModel.numOutboundPeers + maxNumOutboundPeers: nodeModel.maxNumOutboundPeers + } + } + + MouseArea { + anchors.fill: canvas + onClicked: { + pause ? nodeModel.pause = false : nodeModel.pause = true + } + } +} diff --git a/src/qml/controls/Header.qml b/src/qml/controls/Header.qml index 84ef0e771f..587a6ed558 100644 --- a/src/qml/controls/Header.qml +++ b/src/qml/controls/Header.qml @@ -16,6 +16,7 @@ ColumnLayout { property string description: "" property int descriptionMargin: 10 property int descriptionSize: 18 + property string descriptionColor: Theme.color.neutral8 property string subtext: "" property int subtextMargin property int subtextSize: 15 @@ -42,7 +43,7 @@ ColumnLayout { font.family: "Inter" font.styleName: "Regular" font.pixelSize: root.descriptionSize - color: Theme.color.neutral8 + color: root.descriptionColor text: root.description horizontalAlignment: root.center ? Text.AlignHCenter : Text.AlignLeft wrapMode: wrap ? Text.WordWrap : Text.NoWrap diff --git a/src/qml/controls/OptionButton.qml b/src/qml/controls/OptionButton.qml index 97485fb1eb..b40c9b752e 100644 --- a/src/qml/controls/OptionButton.qml +++ b/src/qml/controls/OptionButton.qml @@ -12,7 +12,6 @@ Button { id: button padding: 15 checkable: true - property alias detail: detail_loader.sourceComponent implicitWidth: 450 background: Rectangle { border.width: 1 @@ -67,7 +66,15 @@ Button { } Loader { id: detail_loader - visible: item + visible: button.checked + active: true + sourceComponent: Button { + icon.source: "image://images/check" + icon.color: Theme.color.neutral9 + icon.height: 24 + icon.width: 24 + background: null + } } } } diff --git a/src/qml/imageprovider.cpp b/src/qml/imageprovider.cpp index 7144569447..b93c036f6e 100644 --- a/src/qml/imageprovider.cpp +++ b/src/qml/imageprovider.cpp @@ -62,6 +62,11 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize return QIcon(":/icons/caret-right").pixmap(requested_size); } + if (id == "check") { + *size = requested_size; + return QIcon(":/icons/check").pixmap(requested_size); + } + if (id == "cross") { *size = requested_size; return QIcon(":/icons/cross").pixmap(requested_size); diff --git a/src/qml/nodemodel.cpp b/src/qml/nodemodel.cpp index 862597779d..a10f07c73f 100644 --- a/src/qml/nodemodel.cpp +++ b/src/qml/nodemodel.cpp @@ -5,11 +5,14 @@ #include #include +#include +#include #include #include #include +#include #include #include @@ -17,6 +20,7 @@ NodeModel::NodeModel(interfaces::Node& node) : m_node{node} { ConnectToBlockTipSignal(); + ConnectToNumConnectionsChangedSignal(); } void NodeModel::setBlockTipHeight(int new_height) @@ -27,14 +31,68 @@ void NodeModel::setBlockTipHeight(int new_height) } } +void NodeModel::setNumOutboundPeers(int new_num) +{ + if (new_num != m_num_outbound_peers) { + m_num_outbound_peers = new_num; + Q_EMIT numOutboundPeersChanged(); + } +} + +void NodeModel::setRemainingSyncTime(double new_progress) +{ + int currentTime = QDateTime::currentDateTime().toMSecsSinceEpoch(); + + // keep a vector of samples of verification progress at height + m_block_process_time.push_front(qMakePair(currentTime, new_progress)); + + // show progress speed if we have more than one sample + if (m_block_process_time.size() >= 2) { + double progressDelta = 0; + int timeDelta = 0; + int remainingMSecs = 0; + double remainingProgress = 1.0 - new_progress; + for (int i = 1; i < m_block_process_time.size(); i++) { + QPair sample = m_block_process_time[i]; + + // take first sample after 500 seconds or last available one + if (sample.first < (currentTime - 500 * 1000) || i == m_block_process_time.size() - 1) { + progressDelta = m_block_process_time[0].second - sample.second; + timeDelta = m_block_process_time[0].first - sample.first; + remainingMSecs = (progressDelta > 0) ? remainingProgress / progressDelta * timeDelta : -1; + break; + } + } + if (remainingMSecs > 0 && m_block_process_time.count() % 1000 == 0) { + m_remaining_sync_time = remainingMSecs; + + Q_EMIT remainingSyncTimeChanged(); + } + static const int MAX_SAMPLES = 5000; + if (m_block_process_time.count() > MAX_SAMPLES) { + m_block_process_time.remove(1, m_block_process_time.count() - 1); + } + } +} void NodeModel::setVerificationProgress(double new_progress) { if (new_progress != m_verification_progress) { + setRemainingSyncTime(new_progress); + m_verification_progress = new_progress; Q_EMIT verificationProgressChanged(); } } +void NodeModel::setPause(bool new_pause) +{ + if(m_pause != new_pause) { + m_pause = new_pause; + m_node.setNetworkActive(!new_pause); + Q_EMIT pauseChanged(new_pause); + } +} + void NodeModel::startNodeInitializionThread() { Q_EMIT requestedInitialize(); @@ -45,6 +103,8 @@ void NodeModel::initializeResult([[maybe_unused]] bool success, interfaces::Bloc // TODO: Handle the `success` parameter, setBlockTipHeight(tip_info.block_height); setVerificationProgress(tip_info.verification_progress); + + Q_EMIT setTimeRatioListInitial(); } void NodeModel::startShutdownPolling() @@ -75,6 +135,18 @@ void NodeModel::ConnectToBlockTipSignal() QMetaObject::invokeMethod(this, [=] { setBlockTipHeight(tip.block_height); setVerificationProgress(verification_progress); + + Q_EMIT setTimeRatioList(tip.block_time); }); }); } + +void NodeModel::ConnectToNumConnectionsChangedSignal() +{ + assert(!m_handler_notify_num_peers_changed); + + m_handler_notify_num_peers_changed = m_node.handleNotifyNumConnectionsChanged( + [this](PeersNumByType new_num_peers) { + setNumOutboundPeers(new_num_peers.outbound_full_relay + new_num_peers.block_relay); + }); +} diff --git a/src/qml/nodemodel.h b/src/qml/nodemodel.h index c6a04a1f2c..90b4872e92 100644 --- a/src/qml/nodemodel.h +++ b/src/qml/nodemodel.h @@ -25,15 +25,26 @@ class NodeModel : public QObject { Q_OBJECT Q_PROPERTY(int blockTipHeight READ blockTipHeight NOTIFY blockTipHeightChanged) + Q_PROPERTY(int numOutboundPeers READ numOutboundPeers NOTIFY numOutboundPeersChanged) + Q_PROPERTY(int maxNumOutboundPeers READ maxNumOutboundPeers CONSTANT) + Q_PROPERTY(int remainingSyncTime READ remainingSyncTime NOTIFY remainingSyncTimeChanged) Q_PROPERTY(double verificationProgress READ verificationProgress NOTIFY verificationProgressChanged) + Q_PROPERTY(bool pause READ pause WRITE setPause NOTIFY pauseChanged) public: explicit NodeModel(interfaces::Node& node); int blockTipHeight() const { return m_block_tip_height; } void setBlockTipHeight(int new_height); + int numOutboundPeers() const { return m_num_outbound_peers; } + void setNumOutboundPeers(int new_num); + int maxNumOutboundPeers() const { return m_max_num_outbound_peers; } + int remainingSyncTime() const { return m_remaining_sync_time; } + void setRemainingSyncTime(double new_progress); double verificationProgress() const { return m_verification_progress; } void setVerificationProgress(double new_progress); + bool pause() const { return m_pause; } + void setPause(bool new_pause); Q_INVOKABLE void startNodeInitializionThread(); @@ -45,9 +56,15 @@ public Q_SLOTS: Q_SIGNALS: void blockTipHeightChanged(); + void numOutboundPeersChanged(); + void remainingSyncTimeChanged(); void requestedInitialize(); void requestedShutdown(); void verificationProgressChanged(); + void pauseChanged(bool new_pause); + + void setTimeRatioList(int new_time); + void setTimeRatioListInitial(); protected: void timerEvent(QTimerEvent* event) override; @@ -55,14 +72,22 @@ public Q_SLOTS: private: // Properties that are exposed to QML. int m_block_tip_height{0}; + int m_num_outbound_peers{0}; + static constexpr int m_max_num_outbound_peers{MAX_OUTBOUND_FULL_RELAY_CONNECTIONS + MAX_BLOCK_RELAY_ONLY_CONNECTIONS}; + int m_remaining_sync_time{0}; double m_verification_progress{0.0}; + bool m_pause{false}; int m_shutdown_polling_timer_id{0}; + QVector> m_block_process_time; + interfaces::Node& m_node; std::unique_ptr m_handler_notify_block_tip; + std::unique_ptr m_handler_notify_num_peers_changed; void ConnectToBlockTipSignal(); + void ConnectToNumConnectionsChangedSignal(); }; #endif // BITCOIN_QML_NODEMODEL_H diff --git a/src/qml/options_model.cpp b/src/qml/options_model.cpp new file mode 100644 index 0000000000..ed934121d4 --- /dev/null +++ b/src/qml/options_model.cpp @@ -0,0 +1,46 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include + +#include + +OptionsQmlModel::OptionsQmlModel(interfaces::Node& node) + : m_node{node} +{ + int64_t prune_value{SettingToInt(m_node.getPersistentSetting("prune"), 0)}; + m_prune = (prune_value > 1); + m_prune_size_gb = m_prune ? PruneMiBtoGB(prune_value) : DEFAULT_PRUNE_TARGET_GB; +} + +void OptionsQmlModel::setPrune(bool new_prune) +{ + if (new_prune != m_prune) { + m_prune = new_prune; + m_node.updateRwSetting("prune", pruneSetting()); + Q_EMIT pruneChanged(new_prune); + } +} + +void OptionsQmlModel::setPruneSizeGB(int new_prune_size_gb) +{ + if (new_prune_size_gb != m_prune_size_gb) { + m_prune_size_gb = new_prune_size_gb; + m_node.updateRwSetting("prune", pruneSetting()); + Q_EMIT pruneSizeGBChanged(new_prune_size_gb); + } +} + +util::SettingsValue OptionsQmlModel::pruneSetting() const +{ + assert(!m_prune || m_prune_size_gb >= 1); + return m_prune ? PruneGBtoMiB(m_prune_size_gb) : 0; +} diff --git a/src/qml/options_model.h b/src/qml/options_model.h new file mode 100644 index 0000000000..7e227ee6b1 --- /dev/null +++ b/src/qml/options_model.h @@ -0,0 +1,45 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QML_OPTIONS_MODEL_H +#define BITCOIN_QML_OPTIONS_MODEL_H + +#include + +#include + +namespace interfaces { +class Node; +} + +/** Model for Bitcoin client options. */ +class OptionsQmlModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool prune READ prune WRITE setPrune NOTIFY pruneChanged) + Q_PROPERTY(int pruneSizeGB READ pruneSizeGB WRITE setPruneSizeGB NOTIFY pruneSizeGBChanged) + +public: + explicit OptionsQmlModel(interfaces::Node& node); + + bool prune() const { return m_prune; } + void setPrune(bool new_prune); + int pruneSizeGB() const { return m_prune_size_gb; } + void setPruneSizeGB(int new_prune_size); + +Q_SIGNALS: + void pruneChanged(bool new_prune); + void pruneSizeGBChanged(int new_prune_size_gb); + +private: + interfaces::Node& m_node; + + // Properties that are exposed to QML. + bool m_prune; + int m_prune_size_gb; + + util::SettingsValue pruneSetting() const; +}; + +#endif // BITCOIN_QML_OPTIONS_MODEL_H diff --git a/src/qml/pages/main.qml b/src/qml/pages/main.qml index 008782cf0a..c37825370a 100644 --- a/src/qml/pages/main.qml +++ b/src/qml/pages/main.qml @@ -8,6 +8,7 @@ import QtQuick.Layouts 1.15 import "../components" import "../controls" import "./onboarding" +import "./node" ApplicationWindow { id: appWindow @@ -44,30 +45,29 @@ ApplicationWindow { Component { id: node - Page { + SwipeView { + id: node_swipe anchors.fill: parent - background: null - ColumnLayout { - width: 600 - spacing: 0 - anchors.centerIn: parent - Component.onCompleted: nodeModel.startNodeInitializionThread(); - Image { - Layout.alignment: Qt.AlignCenter - source: "image://images/app" - sourceSize.width: 64 - sourceSize.height: 64 + interactive: false + orientation: Qt.Vertical + NodeRunner { + navRightDetail: NavButton { + iconSource: "image://images/gear" + iconHeight: 24 + onClicked: node_swipe.incrementCurrentIndex() } - BlockCounter { - Layout.alignment: Qt.AlignCenter - blockHeight: nodeModel.blockTipHeight + } + NodeSettings { + navMiddleDetail: Header { + bold: true + headerSize: 18 + header: "Settings" } - ProgressIndicator { - width: 200 - Layout.alignment: Qt.AlignCenter - progress: nodeModel.verificationProgress + navRightDetail: NavButton { + text: qsTr("Done") + onClicked: node_swipe.decrementCurrentIndex() } } - } + } } } diff --git a/src/qml/pages/node/NodeRunner.qml b/src/qml/pages/node/NodeRunner.qml new file mode 100644 index 0000000000..9980e98da9 --- /dev/null +++ b/src/qml/pages/node/NodeRunner.qml @@ -0,0 +1,29 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "../../controls" +import "../../components" + +Page { + background: null + clip: true + property alias navRightDetail: navbar.rightDetail + header: NavigationBar { + id: navbar + } + ColumnLayout { + spacing: 0 + anchors.fill: parent + ColumnLayout { + width: 600 + spacing: 0 + anchors.centerIn: parent + Component.onCompleted: nodeModel.startNodeInitializionThread(); + BlockClockComponent {} + } + } +} diff --git a/src/qml/pages/node/NodeSettings.qml b/src/qml/pages/node/NodeSettings.qml new file mode 100644 index 0000000000..445c8ac2af --- /dev/null +++ b/src/qml/pages/node/NodeSettings.qml @@ -0,0 +1,120 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import "../../controls" +import "../../components" +import "../settings" + +Item { + id: nodeSettings + property alias navMiddleDetail: nodeSettingsView.navMiddleDetail + property alias navRightDetail: nodeSettingsView.navRightDetail + StackView { + id: nodeSettingsView + property alias navMiddleDetail: node_settings.navMiddleDetail + property alias navRightDetail: node_settings.navRightDetail + initialItem: Page { + id: node_settings + property alias navMiddleDetail: navbar.middleDetail + property alias navRightDetail: navbar.rightDetail + background: null + header: NavigationBar { + id: navbar + } + ColumnLayout { + spacing: 0 + width: parent.width + ColumnLayout { + spacing: 20 + Layout.maximumWidth: 450 + Layout.topMargin: 30 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Layout.alignment: Qt.AlignCenter + Setting { + Layout.fillWidth: true + header: qsTr("Dark Mode") + actionItem: OptionSwitch { + checked: Theme.dark + onToggled: Theme.toggleDark() + } + } + Setting { + Layout.fillWidth: true + header: qsTr("About") + actionItem: NavButton { + iconSource: "image://images/caret-right" + background: null + onClicked: { + nodeSettingsView.push(about_page) + } + } + } + Setting { + Layout.fillWidth: true + header: qsTr("Storage") + actionItem: NavButton { + iconSource: "image://images/caret-right" + background: null + onClicked: { + nodeSettingsView.push(storage_page) + } + } + } + Setting { + Layout.fillWidth: true + header: qsTr("Connection") + actionItem: NavButton { + iconSource: "image://images/caret-right" + background: null + onClicked: { + nodeSettingsView.push(connection_page) + } + } + } + } + } + } + anchors.fill: parent + } + Component { + id: about_page + SettingsAbout { + navLeftDetail: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: { + nodeSettingsView.pop() + } + } + } + } + Component { + id: storage_page + SettingsStorage { + navLeftDetail: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: { + nodeSettingsView.pop() + } + } + } + } + Component { + id: connection_page + SettingsConnection { + navLeftDetail: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: { + nodeSettingsView.pop() + } + } + } + } +} diff --git a/src/qml/pages/onboarding/OnboardingCover.qml b/src/qml/pages/onboarding/OnboardingCover.qml index 759638f97e..8e4312ad9c 100644 --- a/src/qml/pages/onboarding/OnboardingCover.qml +++ b/src/qml/pages/onboarding/OnboardingCover.qml @@ -52,14 +52,5 @@ Page { } } } - SettingsDeveloper { - navLeftDetail: NavButton { - iconSource: "image://images/caret-left" - text: qsTr("Back") - onClicked: { - introductions.decrementCurrentIndex() - } - } - } } } diff --git a/src/qml/pages/onboarding/OnboardingStorageLocation.qml b/src/qml/pages/onboarding/OnboardingStorageLocation.qml index 030bbebfc9..c4d2f770f9 100644 --- a/src/qml/pages/onboarding/OnboardingStorageLocation.qml +++ b/src/qml/pages/onboarding/OnboardingStorageLocation.qml @@ -19,7 +19,7 @@ InformationPage { bold: true headerText: qsTr("Storage location") headerMargin: 0 - description: qsTr("Where do you want to store the downloaded block data?") + description: qsTr("Where do you want to store the downloaded block data?\nYou need a minimum of 1GB of storage.") descriptionMargin: 20 detailActive: true detailItem: ColumnLayout { diff --git a/src/qml/pages/settings/SettingsAbout.qml b/src/qml/pages/settings/SettingsAbout.qml index 4f74b2c88d..d6f9e98199 100644 --- a/src/qml/pages/settings/SettingsAbout.qml +++ b/src/qml/pages/settings/SettingsAbout.qml @@ -8,19 +8,40 @@ import QtQuick.Layouts 1.15 import "../../controls" import "../../components" -InformationPage { - bannerActive: false - bold: true - headerText: qsTr("About") - headerMargin: 0 - description: qsTr("Bitcoin Core is an open source project.\nIf you find it useful, please contribute.\n\n This is experimental software.") - descriptionMargin: 20 - detailActive: true - detailItem: ColumnLayout { - spacing: 0 - AboutOptions { - Layout.maximumWidth: 450 - Layout.alignment: Qt.AlignCenter +Item { + property alias navLeftDetail: aboutSwipe.navLeftDetail + SwipeView { + id: aboutSwipe + property alias navLeftDetail: about_settings.navLeftDetail + anchors.fill: parent + interactive: false + orientation: Qt.Horizontal + InformationPage { + id: about_settings + bannerActive: false + bannerMargin: 0 + bold: true + headerText: qsTr("About") + headerMargin: 0 + description: qsTr("Bitcoin Core is an open source project.\nIf you find it useful, please contribute.\n\n This is experimental software.") + descriptionMargin: 20 + detailActive: true + detailItem: ColumnLayout { + spacing: 0 + AboutOptions { + Layout.maximumWidth: 450 + Layout.alignment: Qt.AlignCenter + } + } + } + SettingsDeveloper { + navLeftDetail: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: { + aboutSwipe.decrementCurrentIndex() + } + } } } } diff --git a/src/qml/res/icons/check.png b/src/qml/res/icons/check.png new file mode 100644 index 0000000000..ef408cb502 Binary files /dev/null and b/src/qml/res/icons/check.png differ diff --git a/src/qml/res/src/check.svg b/src/qml/res/src/check.svg new file mode 100644 index 0000000000..a393deb9b4 --- /dev/null +++ b/src/qml/res/src/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index 092ffe7e5b..f0ce083fba 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -245,8 +246,8 @@ void ClientModel::subscribeToCoreSignals() Q_EMIT showProgress(QString::fromStdString(title), progress); }); m_handler_notify_num_connections_changed = m_node.handleNotifyNumConnectionsChanged( - [this](int new_num_connections) { - Q_EMIT numConnectionsChanged(new_num_connections); + [this](PeersNumByType new_num_connections) { + Q_EMIT numConnectionsChanged(new_num_connections.total); }); m_handler_notify_network_active_changed = m_node.handleNotifyNetworkActiveChanged( [this](bool network_active) {