From fa7664720c85df26cc29660ffa5e886734027a90 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 19 Feb 2026 08:51:01 +0400 Subject: [PATCH 01/13] Temporary commit --- CMakeLists.txt | 3 +- src/StorageBackend.cpp | 51 +++++++++++++++++ src/StorageBackend.h | 2 + src/qml/CMakeLists.txt | 3 + src/qml/Main.qml | 109 +++++++++++++++++++---------------- src/qml/OnBoarding.qml | 122 ++++++++++++++++++---------------------- src/qml/StartNode.qml | 46 ++++++++++++--- src/qml/StorageView.qml | 75 ++++-------------------- src/qml/main.cpp | 3 + 9 files changed, 226 insertions(+), 188 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9970423..791805a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,7 +130,7 @@ endif() # Discover the required dependencies. # Without this discovery part, the dependencies cannot be found. # COMPONENTS is kind of generic for Qt modules. -find_package(Qt6 REQUIRED COMPONENTS Core Widgets RemoteObjects Quick QuickWidgets) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets RemoteObjects Quick QuickWidgets Network) # Get the real path to handle symlinks correctly #get_filename_component(REAL_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH) @@ -419,6 +419,7 @@ target_link_libraries(storage_ui PRIVATE Qt6::RemoteObjects Qt6::Quick Qt6::QuickWidgets + Qt6::Network component-interfaces ) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index 592b3f7..b61432b 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include // StorageBackend is responsible for managing the interaction with the storage module. // It is mocked in the QML. @@ -866,6 +868,55 @@ QString StorageBackend::buildConfig(const QString& dataDir, int discPort, int tc return QJsonDocument(obj).toJson(QJsonDocument::Indented); } +QString StorageBackend::buildUpnpConfig(const QString& dataDir) { + debug("StorageBackend::buildUpnpConfig called with dataDir=" + dataDir); + + QJsonDocument doc = QJsonDocument::fromJson(m_configJson.toUtf8()); + QJsonObject obj = doc.object(); + + obj["data-dir"] = dataDir; + + QJsonArray bootstrapArray; + for (const QString& node : BOOTSTRAP_NODES) { + bootstrapArray.append(node); + } + obj["nat"] = "upnp"; + + return QJsonDocument(obj).toJson(QJsonDocument::Indented); +} + +QString StorageBackend::buildNatExtConfig(const QString& dataDir, int tcpPort) { + debug("StorageBackend::buildUpnpConfig called with dataDir=" + dataDir + + " and tcpPort=" + QString::number(tcpPort)); + + QJsonDocument doc = QJsonDocument::fromJson(m_configJson.toUtf8()); + QJsonObject obj = doc.object(); + + obj["data-dir"] = dataDir; + + QJsonArray bootstrapArray; + for (const QString& node : BOOTSTRAP_NODES) { + bootstrapArray.append(node); + } + + debug("Retrieving the public IP"); + + QNetworkAccessManager manager; + QEventLoop loop; + QNetworkReply* reply = manager.get(QNetworkRequest(QUrl("https://echo.codex.storage/"))); + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + QString ip = reply->readAll().trimmed(); + reply->deleteLater(); + + debug("Public IP detected:" + ip); + + obj["nat"] = "extip:" + ip; + + return QJsonDocument(obj).toJson(QJsonDocument::Indented); +} + QString StorageBackend::buildConfigFromFile(const QString& path) { qDebug() << "StorageBackend::buildConfigFromFile called"; diff --git a/src/StorageBackend.h b/src/StorageBackend.h index e46ed89..d31109c 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -85,6 +85,8 @@ class StorageBackend : public QObject { void reloadIfChanged(const QString& configJson); void status(StorageStatus status); QString buildConfig(const QString& dataDir, int discPort, int tcpPort); + QString buildUpnpConfig(const QString& dataDir); + QString buildNatExtConfig(const QString& dataDir, int tcpPort); QString buildConfigFromFile(const QString& path); void saveUserConfig(const QString& configJson); diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index a3073a4..e08a675 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -132,6 +132,9 @@ qt_add_qml_module(appqml StartNode.qml LogosTextField.qml LogosStorageButton.qml + Nat.qml + LogosStorageLayout.qml + PortForwarding.qml ) # Set up QML module directory for runtime diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 1a13bf0..01c193e 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -1,50 +1,28 @@ import QtQuick import QtQuick.Controls import QtCore -import Logos.Theme -import Logos.Controls // qmllint disable unqualified Item { id: root - implicitWidth: 600 - implicitHeight: 400 + implicitWidth: 800 + implicitHeight: 800 property var backend: mockBackend - // Timer { - // readonly property int running: 2 - - // id: timer - // interval: 2000 - // repeat: false - // onTriggered: { - // console.log("timer triggered") - // // root.backend.status = running - // // root.backend.startCompleted() - // // console.info(root.backend.status) - // } - // } QtObject { id: mockBackend + readonly property bool isMock: true property int status signal startCompleted signal startFailed signal stopCompleted - - function updateBasicConfig(dataDir, discPort) { - console.log("updateBasicConfig", dataDir, discPort) - } + signal initCompleted function start() { - // timer.start() - console.log("mock start callde") - } - - function stop() { - root.backend.stopCompleted() + console.log("mock start called") } function defaultDataDir() { @@ -53,9 +31,15 @@ Item { function buildConfig() {} + function saveUserConfig() {} + function reloadIfChanged() {} - function init() {} + function buildUpnpConfig() {} + + function buildNatExtConfig() {} + + function stop() {} } Settings { @@ -66,59 +50,90 @@ Item { property int tcpPort: 0 property string dataDir: "" property bool onboardingCompleted: false + property string natStrategy: "any" } StackView { id: stackView anchors.fill: parent - initialItem: onboarding + initialItem: onboardingComponent } Component { - id: onboarding + id: onboardingComponent OnBoarding { - id: onboardingInstance backend: root.backend - discoveryPort: settings.discoveryPort - tcpPort: settings.tcpPort - dataDir: settings.dataDir.length > 0 ? settings.dataDir : root.backend.defaultDataDir() + // discoveryPort: settings.discoveryPort + // tcpPort: settings.tcpPort + dataDir: settings.dataDir onCompleted: { - settings.discoveryPort = discoveryPort + // settings.discoveryPort = discoveryPort settings.dataDir = dataDir - settings.tcpPort = tcpPort + // settings.tcpPort = tcpPort settings.onboardingCompleted = true - let config = root.backend.buildConfig(dataDir, - discoveryPort, tcpPort) - root.backend.saveUserConfig(config) - root.backend.reloadIfChanged(config) - root.backend.start() - - stackView.push(startNodeView) + stackView.push(natComponent) } } } Component { - id: storageView + id: natComponent + + Nat { + onCompleted: function (enabled) { + if (enabled) { + settings.natStrategy = "upnp" + let config = root.backend.buildUpnpConfig(settings.dataDir) + root.backend.reloadIfChanged(config) + root.backend.start() + stackView.push(startNodeComponent) + } else { + stackView.push(portForwardingComponent) + } + } + } + } + + Component { + id: storageComponent + StorageView { backend: root.backend } } Component { - id: startNodeView + id: startNodeComponent StartNode { backend: root.backend onBack: { root.backend.stop() + stackView.pop() } + onNext: { - stackView.push(storageView) + stackView.push(storageComponent) + } + } + } + + Component { + id: portForwardingComponent + + PortForwarding { + onPortTcpSelected: function (port) { + settings.tcpPort = port + settings.natStrategy = "extip" + let config = root.backend.buildNatExtConfig(settings.dataDir, + port) + root.backend.reloadIfChanged(config) + root.backend.start() + stackView.push(startNodeComponent) } } } @@ -133,7 +148,7 @@ Item { function onInitCompleted() { if (settings.onboardingCompleted) { root.backend.start() - stackView.replace(storageView, StackView.Immediate) + stackView.replace(storageComponent, StackView.Immediate) } } } diff --git a/src/qml/OnBoarding.qml b/src/qml/OnBoarding.qml index c60464c..e3fef3c 100644 --- a/src/qml/OnBoarding.qml +++ b/src/qml/OnBoarding.qml @@ -1,22 +1,18 @@ import QtQuick -import QtQuick.Controls import QtQuick.Dialogs import QtQuick.Layouts import Logos.Theme import Logos.Controls -Rectangle { +LogosStorageLayout { id: root - color: Theme.palette.background - Layout.fillWidth: true - Layout.fillHeight: true - implicitWidth: 600 - implicitHeight: 400 property int discoveryPort: 8090 property int tcpPort: 0 property var backend: mockBackend + property var local: false property string dataDir: backend.defaultDataDir() + signal completed QtObject { @@ -30,79 +26,52 @@ Rectangle { ColumnLayout { anchors.centerIn: parent spacing: Theme.spacing.medium - width: 400 LogosText { id: titleText font.pixelSize: Theme.typography.titleText text: "Logos Storage" - // anchors.verticalCenter: parent.verticalCenter + Layout.alignment: Qt.AlignCenter } - ColumnLayout { - id: discoveryPortColumn - spacing: Theme.spacing.tiny - Layout.fillWidth: true - - LogosText { - text: "Discovery port" - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.text - } - - LogosTextField { - isValid: acceptableInput && text.length > 0 - id: discoveryPortTextField - placeholderText: "Enter the discovery port" - text: root.discoveryPort - validator: IntValidator { - bottom: 1 - top: 65535 - } - onTextChanged: { - if (isValid) { - root.discoveryPort = parseInt(text) - } - } - } + LogosText { + id: questionText + font.pixelSize: Theme.typography.titleText + text: "First, let's choose the storage folder" + Layout.alignment: Qt.AlignCenter } - ColumnLayout { - id: tcpPortColumn - spacing: Theme.spacing.tiny - Layout.fillWidth: true + // ColumnLayout { + // id: discoveryPortColumn + // spacing: Theme.spacing.tiny + // Layout.fillWidth: true - LogosText { - text: "TCP port" - font.pixelSize: Theme.typography.secondaryText - color: Theme.palette.text - } - - LogosTextField { - isValid: acceptableInput && text.length > 0 - id: tcpPortTextField - placeholderText: "Enter the TCP port" - text: root.tcpPort - validator: IntValidator { - bottom: 0 - top: 65535 - } - onTextChanged: { - if (isValid) { - root.tcpPort = parseInt(text) - } - } - } - } + // LogosText { + // text: "Discovery port" + // font.pixelSize: Theme.typography.secondaryText + // color: Theme.palette.text + // } + // LogosTextField { + // isValid: acceptableInput && text.length > 0 + // id: discoveryPortTextField + // placeholderText: "Enter the discovery port" + // text: root.discoveryPort + // validator: IntValidator { + // bottom: 1 + // top: 65535 + // } + // onTextChanged: { + // if (isValid) { + // root.discoveryPort = parseInt(text) + // } + // } + // } + // } ColumnLayout { spacing: Theme.spacing.tiny Layout.fillWidth: true - LogosText { - text: "Data dir" - } - RowLayout { spacing: Theme.spacing.tiny @@ -130,6 +99,20 @@ Rectangle { } } } + + // Column { + // CheckBox { + // text: "Do you want to connect to a local network ?" + // checked: false + // onCheckedChanged: root.local = checked + // } + + // LogosText { + // font.pixelSize: Theme.typography.secondaryText + // text: "You will not " + // Layout.alignment: Qt.AlignCenter + // } + // } } LogosStorageButton { @@ -138,8 +121,11 @@ Rectangle { anchors.right: parent.right anchors.bottomMargin: 10 anchors.rightMargin: 10 - enabled: discoveryPortTextField.acceptableInput - && tcpPortTextField.acceptableInput && dataDirTextField.isValid - onClicked: root.completed() + enabled: dataDirTextField.isValid + // enabled: discoveryPortTextField.acceptableInput + // && tcpPortTextField.acceptableInput && dataDirTextField.isValid + onClicked: function () { + root.completed() + } } } diff --git a/src/qml/StartNode.qml b/src/qml/StartNode.qml index f218c97..9beddda 100644 --- a/src/qml/StartNode.qml +++ b/src/qml/StartNode.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import QtQuick.Controls import Logos.Controls import Logos.Theme @@ -11,40 +12,67 @@ Rectangle { implicitWidth: 600 implicitHeight: 400 - property var backend + property var backend: mockBackend property string status: "" + property string title: "Starting your node...." property bool starting: true property bool success: false signal back signal next + function onNodeStarted() { + root.starting = false + root.status = "Logos Storage started successfully." + root.title = "Success" + root.success = true + } + + QtObject { + id: mockBackend + + readonly property bool isMock: true + property string configJson: "{}" + + signal startCompleted + signal startFailed + signal nodeStarted + } + + Timer { + interval: 2000 + running: root.backend && root.backend.isMock === true + onTriggered: { + console.log("timer triggered") + root.onNodeStarted() + } + } + Connections { target: root.backend function onStartCompleted() { - console.log("onStartCompleted received") - root.starting = false - root.status = "Logos Storage started successfully." - root.success = true + console.log("onStartCompleted") + root.onNodeStarted() } function onStartFailed(error) { - console.log("onStartFailed received") root.starting = false + root.title = "Error" root.status = "Failed to start: " + error } } ColumnLayout { - anchors.centerIn: parent + anchors.fill: parent + anchors.margins: 20 + anchors.bottomMargin: 60 spacing: Theme.spacing.medium - width: 400 LogosText { id: titleText font.pixelSize: Theme.typography.titleText - text: "Starting your node...." + text: root.title Layout.alignment: Qt.AlignHCenter } diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index f7cebda..4e45207 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -4,12 +4,13 @@ import QtQuick.Dialogs import QtQuick.Layouts import QtCore +// qmllint disable unqualified Rectangle { id: root Layout.fillWidth: true Layout.fillHeight: true - implicitWidth: 600 - implicitHeight: 600 + implicitWidth: 800 + implicitHeight: 800 color: "#000000" property var backend: mockBackend @@ -63,9 +64,12 @@ Rectangle { property var status: root.stopped property var debugLogs: "Hello !" property var configJson: "{}" - property url cid: "" property string uploadStatus: "" property int uploadProgress: 0 + property var manifests: [] + property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 // 20 GB default + property var quotaUsedBytes: 0 + property var quotaReservedBytes: 0 function start(newConfigJson) { status = root.running @@ -74,63 +78,6 @@ Rectangle { function stop() { status = root.stopped } - - function tryPeerConnect(peerId) { - console.log("Attempting peer connection...") - } - - function tryDebug() { - console.log("Attempting peer connection...") - } - - function spr() {} - - function showPeerId() {} - - function version() {} - - function dataDir() {} - - function tryUploadFinalize() { - console.log("Attempting upload finalize") - } - - function tryUploadFile(file) { - console.log("Attempting upload file") - } - - function tryDownloadFile(cid, file) { - console.log("Attempting download a file", cid, file) - } - - function exists(cid) { - console.log("Attempting exists", cid) - } - - function fetch(cid) { - console.log("Attempting fetch", cid) - } - - function remove(cid) { - console.log("Attempting remove", cid) - } - - function downloadManifest(cid) { - console.log("Attempting downloadManifest", cid) - } - - function downloadManifests() { - console.log("Attempting downloadManifests") - } - - function space() {} - - function updateLogLevel(logLevel) {} - - property var manifests: [] - property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 // 20 GB default - property var quotaUsedBytes: 0 - property var quotaReservedBytes: 0 } function formatBytes(bytes) { @@ -157,13 +104,15 @@ Rectangle { } Button { + property var isStopped: root.backend.status == root.stopped + id: startStopButton objectName: "startStopButton" anchors.leftMargin: 50 text: root.startStopText() enabled: root.canStartStop() - onClicked: root.backend.status == root.stopped ? root.backend.start( - jsonEditor.text) : root.backend.stop() + onClicked: isStopped ? root.backend.start( + jsonEditor.text) : root.backend.stop() anchors.horizontalCenter: parent.horizontalCenter anchors.top: statusTextElement.bottom anchors.topMargin: 10 @@ -216,7 +165,7 @@ Rectangle { implicitHeight: 6 Rectangle { - width: parent.width * parent.parent.visualPosition + width: parent.width * control.visualPosition height: parent.height radius: 3 color: "#4CAF50" diff --git a/src/qml/main.cpp b/src/qml/main.cpp index 1a59950..5666f0b 100644 --- a/src/qml/main.cpp +++ b/src/qml/main.cpp @@ -1,6 +1,9 @@ #include #include #include +#include + +static QQmlTriviallyDestructibleDebuggingEnabler enabler; int main(int argc, char* argv[]) { QGuiApplication app(argc, argv); From 24da3fd5de303e89b4b5b3ac8d366341925da64f Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 19 Feb 2026 08:51:16 +0400 Subject: [PATCH 02/13] Add missing files --- src/qml/LogosStorageLayout.qml | 12 +++++++ src/qml/Nat.qml | 45 ++++++++++++++++++++++++++ src/qml/PortForwarding.qml | 58 ++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 src/qml/LogosStorageLayout.qml create mode 100644 src/qml/Nat.qml create mode 100644 src/qml/PortForwarding.qml diff --git a/src/qml/LogosStorageLayout.qml b/src/qml/LogosStorageLayout.qml new file mode 100644 index 0000000..0ca2ac8 --- /dev/null +++ b/src/qml/LogosStorageLayout.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme + +Rectangle { + id: root + color: Theme.palette.background + Layout.fillWidth: true + Layout.fillHeight: true + implicitWidth: 600 + implicitHeight: 400 +} diff --git a/src/qml/Nat.qml b/src/qml/Nat.qml new file mode 100644 index 0000000..d3a9f0f --- /dev/null +++ b/src/qml/Nat.qml @@ -0,0 +1,45 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +LogosStorageLayout { + id: root + + signal completed(bool enabled) + + ColumnLayout { + anchors.centerIn: parent + spacing: Theme.spacing.medium + width: 400 + + LogosText { + id: questionText + font.pixelSize: Theme.typography.titleText + text: "Is UPnP enabled on your router ?" + Layout.alignment: Qt.AlignCenter + } + + LogosText { + id: questionDescriptionText + font.pixelSize: Theme.typography.primaryText + text: "UPnP simplifies configuration by handling port forwarding automatically." + Layout.alignment: Qt.AlignCenter + } + + RowLayout { + spacing: Theme.spacing.medium + Layout.alignment: Qt.AlignCenter + + LogosStorageButton { + text: "No / I don't know" + onClicked: root.completed(false) + } + + LogosStorageButton { + text: "Yes, I use UPnP" + onClicked: root.completed(true) + } + } + } +} diff --git a/src/qml/PortForwarding.qml b/src/qml/PortForwarding.qml new file mode 100644 index 0000000..b6102a1 --- /dev/null +++ b/src/qml/PortForwarding.qml @@ -0,0 +1,58 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +LogosStorageLayout { + id: root + + property var tcpPort: 0 + + signal portTcpSelected(int port) + + ColumnLayout { + anchors.centerIn: parent + spacing: Theme.spacing.medium + width: 400 + + LogosText { + id: questionText + font.pixelSize: Theme.typography.titleText + text: "Choose your TCP port" + Layout.alignment: Qt.AlignCenter + } + + LogosText { + id: questionDescriptionText + font.pixelSize: Theme.typography.primaryText + text: "The TCP port has to be open to connect with other remote peers." + Layout.alignment: Qt.AlignCenter + } + + LogosTextField { + isValid: acceptableInput && text.length > 0 + id: tcpPortTextField + placeholderText: "Enter the TCP port" + text: root.tcpPort + validator: IntValidator { + bottom: 0 + top: 65536 + } + onTextChanged: { + if (isValid) { + root.tcpPort = parseInt(text) + } + } + } + } + + LogosStorageButton { + text: "Next" + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.bottomMargin: 10 + anchors.rightMargin: 10 + enabled: tcpPortTextField.isValid + onClicked: root.portTcpSelected(root.tcpPort) + } +} From 6e584761767108ff931db937f4c014a72e15f879 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 19 Feb 2026 08:55:06 +0400 Subject: [PATCH 03/13] Update dependencies --- .gitmodules | 3 +++ logos-design-system | 1 + vendor/logos-storage-module | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 160000 logos-design-system diff --git a/.gitmodules b/.gitmodules index 1611f4b..0d3934b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "vendor/logos-storage-nim"] path = vendor/logos-storage-nim url = https://github.com/logos-storage/logos-storage-nim +[submodule "logos-design-system"] + path = logos-design-system + url = https://github.com/logos-co/logos-design-system diff --git a/logos-design-system b/logos-design-system new file mode 160000 index 0000000..596811c --- /dev/null +++ b/logos-design-system @@ -0,0 +1 @@ +Subproject commit 596811cbb0a0644322267368e87fab80e34203d8 diff --git a/vendor/logos-storage-module b/vendor/logos-storage-module index 7dceb6a..b4ecf7a 160000 --- a/vendor/logos-storage-module +++ b/vendor/logos-storage-module @@ -1 +1 @@ -Subproject commit 7dceb6a8dfd5a5bbd47dc9b00e55f80b62ab1e6e +Subproject commit b4ecf7a871233608f63b817eeae426f6273695d9 From 340d3f774f4f3679d0fe4fbda77706c66fd96ab3 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 19 Feb 2026 14:15:16 +0400 Subject: [PATCH 04/13] Refactor configuration management --- src/StorageBackend.cpp | 203 +++++++++++++------------------ src/StorageBackend.h | 70 +++++++++-- src/StorageUIPlugin.cpp | 30 ++--- src/qml/CMakeLists.txt | 1 - src/qml/Main.qml | 74 +++++------- src/qml/Nat.qml | 45 ------- src/qml/OnBoarding.qml | 242 ++++++++++++++++++++++--------------- src/qml/PortForwarding.qml | 19 ++- src/qml/StartNode.qml | 12 +- src/storage_resources.qrc | 2 + 10 files changed, 338 insertions(+), 360 deletions(-) delete mode 100644 src/qml/Nat.qml diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index b61432b..34393e9 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -10,6 +10,7 @@ #include #include #include +#include // StorageBackend is responsible for managing the interaction with the storage module. // It is mocked in the QML. @@ -30,6 +31,8 @@ StorageBackend::StorageBackend(LogosAPI* logosAPI, QObject* parent) } m_logos = new LogosModules(m_logosAPI); + + emit ready(); } StorageBackend::~StorageBackend() @@ -38,20 +41,24 @@ StorageBackend::~StorageBackend() m_logos = nullptr; } -LogosResult StorageBackend::init(const QString& configJson = "{}") { +LogosResult StorageBackend::init(const QString& configJson) { qDebug() << "StorageBackend::initStorage called"; - if (configJson != "{}") { - m_configJson = configJson; + m_config = QJsonDocument::fromJson(configJson.toUtf8()); + if (m_config.isNull()) { + qDebug() << "StorageBackend::initStorage invalid json config" << configJson; + emit initFailed(); + return {false, "", "Failed to create the storage, invalid json config"}; } - bool result = m_logos->storage_module.init(m_configJson); + bool result = m_logos->storage_module.init(configJson); qDebug() << "StorageBackend::initStorage: init"; if (!result) { setStatus(Destroyed); debug("Failed to init storage"); + emit initFailed(); return {false, "", "Filed to init storage"}; } @@ -197,13 +204,8 @@ LogosResult StorageBackend::init(const QString& configJson = "{}") { qWarning() << "StorageWidget: failed to subscribe to storageDownloadProgress events"; } - if (configJson != "{}") { - m_configJson = configJson; - emit configJsonChanged(); - debug("new config is: " + m_configJson); - } - emit initCompleted(); + debug("new config is: " + configJson); return {true, ""}; } @@ -725,12 +727,7 @@ void StorageBackend::space() { static constexpr qint64 DEFAULT_QUOTA = 20LL * 1024 * 1024 * 1024; // 20 GB // Check config for a quota-max-bytes override - qint64 configQuota = 0; - QJsonDocument doc = QJsonDocument::fromJson(m_configJson.toUtf8()); - if (!doc.isNull()) { - configQuota = doc.object().value("quota-max-bytes").toVariant().toLongLong(); - } - + qint64 configQuota = m_config.object().value("quota-max-bytes").toVariant().toLongLong(); qint64 apiQuota = result.getInt("quotaMaxBytes"); m_quotaMaxBytes = apiQuota > 0 ? apiQuota : (configQuota > 0 ? configQuota : DEFAULT_QUOTA); m_quotaUsedBytes = result.getInt("quotaUsedBytes"); @@ -764,30 +761,26 @@ StorageBackend::StorageStatus StorageBackend::status() const { return m_status; QString StorageBackend::cid() const { return m_cid; } -QString StorageBackend::configJson() const { return m_configJson; } +QString StorageBackend::configJson() const { return QString::fromUtf8(m_config.toJson(QJsonDocument::Compact)); } int StorageBackend::uploadProgress() const { return m_uploadProgress; } QString StorageBackend::uploadStatus() const { return m_uploadStatus; } void StorageBackend::reloadIfChanged(const QString& configJson) { - if (configJson == m_configJson) { + QJsonDocument config = QJsonDocument::fromJson(configJson.toUtf8()); + if (config.isNull()) { + debug("Invalid json detected !"); + return; + } + + if (m_config == config) { debug("No change detected in the config"); return; } debug("New config detected"); - QJsonDocument doc = QJsonDocument::fromJson(configJson.toUtf8()); - if (doc.isNull()) { - debug("Invalid json detected !"); - - m_configJson = configJson; - emit configJsonChanged(); - - return; - } - if (m_status == StorageStatus::Running || m_status == StorageStatus::Stopping || m_status == StorageStatus::Starting) { debug("Cannot reload the config while running, stopping or starting..."); @@ -805,154 +798,124 @@ void StorageBackend::reloadIfChanged(const QString& configJson) { } } - bool result = m_logos->storage_module.init(configJson); + LogosResult result = init(configJson); - if (!result) { - debug("Failed to init context with new config, will rollback."); - - bool result = m_logos->storage_module.init(m_configJson); - - if (!result) { - debug("Failed to init context with old config, that's a serious issue."); - } else { - debug("Old config restored"); - setStatus(StorageStatus::Stopped); - - m_configJson = configJson; - emit configJsonChanged(); - } + if (!result.success) { + debug("Failed to init context with new config: " + result.getError()); return; } debug("New config loaded successfully"); - m_configJson = configJson; + m_config = config; setStatus(StorageStatus::Stopped); - emit configJsonChanged(); +} + +void StorageBackend::saveCurrentConfig() { + qDebug() << "StorageBackend::saveUserConfig"; + saveUserConfig(QString::fromUtf8(m_config.toJson(QJsonDocument::Compact))); } void StorageBackend::saveUserConfig(const QString& configJson) { qDebug() << "StorageBackend::saveUserConfig"; - QString configPath = getUserConfigPath(); - QString folderPath = QFileInfo(configPath).absolutePath(); + QString folderPath = QFileInfo(USER_CONFIG_PATH).absolutePath(); QDir().mkpath(folderPath); - QFile file(configPath); + QFile file(USER_CONFIG_PATH); if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { file.write(configJson.toUtf8()); file.close(); - debug("Config saved to " + configPath); + debug("Config saved to " + USER_CONFIG_PATH); } else { - debug("Failed to save config to " + configPath); + debug("Failed to save config to " + USER_CONFIG_PATH); } } -QString StorageBackend::buildConfig(const QString& dataDir, int discPort, int tcpPort) { - debug("StorageBackend::updateBasicConfig called with dataDir=" + dataDir); - - QJsonDocument doc = QJsonDocument::fromJson(m_configJson.toUtf8()); +QJsonDocument StorageBackend::defaultConfig() { + QJsonDocument doc = QJsonDocument(); QJsonObject obj = doc.object(); - obj["data-dir"] = dataDir; - obj["disc-port"] = discPort; - - QJsonArray listenAddrs = {QString("/ip4/0.0.0.0/tcp/%1").arg(tcpPort)}; - obj["listen-addrs"] = listenAddrs; - QJsonArray bootstrapArray; for (const QString& node : BOOTSTRAP_NODES) { bootstrapArray.append(node); } obj["bootstrap-node"] = bootstrapArray; - return QJsonDocument(obj).toJson(QJsonDocument::Indented); + obj["data-dir"] = DEFAULT_DATA_DIR; + + return QJsonDocument(obj); } -QString StorageBackend::buildUpnpConfig(const QString& dataDir) { - debug("StorageBackend::buildUpnpConfig called with dataDir=" + dataDir); +void StorageBackend::enableUpnpConfig() { + debug("StorageBackend::enableUpnpConfig called"); - QJsonDocument doc = QJsonDocument::fromJson(m_configJson.toUtf8()); + QJsonDocument doc = defaultConfig(); QJsonObject obj = doc.object(); - obj["data-dir"] = dataDir; - - QJsonArray bootstrapArray; - for (const QString& node : BOOTSTRAP_NODES) { - bootstrapArray.append(node); - } obj["nat"] = "upnp"; - return QJsonDocument(obj).toJson(QJsonDocument::Indented); + reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact))); } -QString StorageBackend::buildNatExtConfig(const QString& dataDir, int tcpPort) { - debug("StorageBackend::buildUpnpConfig called with dataDir=" + dataDir + - " and tcpPort=" + QString::number(tcpPort)); +void StorageBackend::enableNatExtConfig(int tcpPort) { + qDebug() << "StorageBackend::enableNatExtConfig called with tcpPort" << tcpPort; - QJsonDocument doc = QJsonDocument::fromJson(m_configJson.toUtf8()); + QJsonDocument doc = defaultConfig(); QJsonObject obj = doc.object(); - obj["data-dir"] = dataDir; + QJsonArray listenAddrs = {QString("/ip4/0.0.0.0/tcp/%1").arg(tcpPort)}; + obj["listen-addrs"] = listenAddrs; - QJsonArray bootstrapArray; - for (const QString& node : BOOTSTRAP_NODES) { - bootstrapArray.append(node); - } + qDebug() << "StorageBackend::enableNatExtConfig Retrieving the public IP"; - debug("Retrieving the public IP"); + // Create the network manager + QNetworkAccessManager* manager = new QNetworkAccessManager(this); + QNetworkRequest request(QUrl("https://echo.codex.storage/")); + request.setRawHeader("Accept", "text/plain"); + QNetworkReply* reply = manager->get(request); - QNetworkAccessManager manager; - QEventLoop loop; - QNetworkReply* reply = manager.get(QNetworkRequest(QUrl("https://echo.codex.storage/"))); - QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - loop.exec(); + connect(reply, &QNetworkReply::finished, this, [this, reply, manager, obj]() { + reply->deleteLater(); + manager->deleteLater(); - QString ip = reply->readAll().trimmed(); - reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) { + emit natExtConfigFailed(reply->errorString()); + return; + } - debug("Public IP detected:" + ip); + QString ip = reply->readAll().trimmed(); - obj["nat"] = "extip:" + ip; + qDebug() << "StorageBackend::enableNatExtConfig ip=" << ip; - return QJsonDocument(obj).toJson(QJsonDocument::Indented); -} + obj["nat"] = "extip:" + ip; -QString StorageBackend::buildConfigFromFile(const QString& path) { - qDebug() << "StorageBackend::buildConfigFromFile called"; + qDebug() << "StorageBackend::enableNatExtConfig config=" + << QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); - QFile file(path); - if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QString configJson = QString::fromUtf8(file.readAll()); + reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact))); - debug("StorageUIPlugin: config.json is found, configJson=" + configJson); - - return configJson; - } - - debug("StorageUIPlugin: Failed to load config.json"); - return "{}"; + emit natExtConfigCompleted(); + }); } void StorageBackend::status(StorageStatus status) { m_status = status; } -QString StorageBackend::getUserConfigPath() { return QDir::homePath() + "/.logos_storage/config.json"; } +void StorageBackend::loadUserConfig() { + qDebug() << "StorageBackend::loadUserConfig called."; + + QFile file(USER_CONFIG_PATH); + LogosResult result; -QString StorageBackend::getUserConfig() { - QFile file(getUserConfigPath()); if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) { - return QString::fromUtf8(file.readAll()); + result = init(QString::fromUtf8(file.readAll())); + } else { + qWarning() << "StorageBackend::loadUserConfig Failed to read the user config file, fallback to default config"; + result = init(QString::fromUtf8(defaultConfig().toJson(QJsonDocument::Compact))); } - return "{}"; -} - -QString StorageBackend::defaultDataDir() { - QString home = QDir::homePath(); -#ifdef Q_OS_WIN - return home + "/AppData/Roaming/Storage"; -#elif defined(Q_OS_MACOS) - return home + "/Library/Application Support/Storage"; -#else - return home + "/.cache/storage"; -#endif + if (!result.success) { + qWarning() << "StorageBackend::loadUserConfig Failed to load the user config: " + result.getError(); + } else { + debug("User config loaded successfully"); + } } diff --git a/src/StorageBackend.h b/src/StorageBackend.h index d31109c..39db7f7 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -1,6 +1,7 @@ #pragma once #include "logos_api.h" #include "logos_sdk.h" +#include #include #include #include @@ -10,6 +11,10 @@ static const int RET_OK = 0; static const int RET_PROGRESS = 3; +static const QUrl ECHO_PROVIDER("https://echo.codex.storage/"); +static const QString APP_HOME = QDir::homePath() + "/.logos_storage"; +static const QString DEFAULT_DATA_DIR = APP_HOME + "/data"; +static const QString USER_CONFIG_PATH = APP_HOME + "/config.json"; // Add manual SPR from https://spr.codex.storage/devnet static const QStringList BOOTSTRAP_NODES = { @@ -30,7 +35,6 @@ class StorageBackend : public QObject { Q_PROPERTY(QString debugLogs READ debugLogs NOTIFY debugLogsChanged) Q_PROPERTY(StorageStatus status READ status WRITE status NOTIFY statusChanged) Q_PROPERTY(QString cid READ cid NOTIFY cidChanged) - Q_PROPERTY(QString configJson READ configJson NOTIFY configJsonChanged) Q_PROPERTY(int uploadProgress READ uploadProgress NOTIFY uploadProgressChanged) Q_PROPERTY(QString uploadStatus READ uploadStatus NOTIFY uploadStatusChanged) Q_PROPERTY(QVariantList manifests READ manifests NOTIFY manifestsChanged) @@ -39,7 +43,20 @@ class StorageBackend : public QObject { Q_PROPERTY(qint64 quotaReservedBytes READ quotaReservedBytes NOTIFY quotaChanged) public: - enum StorageStatus { Stopped = 0, Starting, Running, Stopping, Destroyed }; + enum StorageStatus { + // Stopped means that the context is created but the module is not started + Stopped = 0, + + Starting, + + // Running means the module is started + Running, + + Stopping, + + // Destroyed means the context is not created (or has been destroyed). + Destroyed + }; Q_ENUM(StorageStatus) QString cid() const; @@ -53,9 +70,7 @@ class StorageBackend : public QObject { qint64 quotaUsedBytes() const; qint64 quotaReservedBytes() const; - Q_INVOKABLE static QString defaultDataDir(); - static QString getUserConfig(); - static QString getUserConfigPath(); + static QJsonDocument defaultConfig(); explicit StorageBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr); ~StorageBackend(); @@ -82,27 +97,58 @@ class StorageBackend : public QObject { void space(); LogosResult init(const QString& configJson); void updateLogLevel(const QString& logLevel); - void reloadIfChanged(const QString& configJson); void status(StorageStatus status); - QString buildConfig(const QString& dataDir, int discPort, int tcpPort); - QString buildUpnpConfig(const QString& dataDir); - QString buildNatExtConfig(const QString& dataDir, int tcpPort); - QString buildConfigFromFile(const QString& path); + + // Save the user config passed in parameter + // into the user config json. void saveUserConfig(const QString& configJson); + // Save the current config object + // into the user config json. + void saveCurrentConfig(); + + // Load the user config saved previously + void loadUserConfig(); + + // Take a new config json and reload the Storage context + // if the configuration has changed. + // + // This method cannot be used if the Storage Module + // is running, starting or stopping. + // + // If the Storage Module was already created, + // it will be destroyed first. + // + // On success, the status will be set to Stopped. + // + // Emit initCompleted on success. + // Emit initFailed on failure. + void reloadIfChanged(const QString& configJson); + + // Enables the upnp in the config + // and re-create a context with the new configuration + void enableUpnpConfig(); + + // Enables the net external in the config + // and re-create a context with the new configuration + void enableNatExtConfig(int tcpPort); + signals: + void ready(); void startCompleted(); void startFailed(const QString& error); void statusChanged(); void debugLogsChanged(); void stopCompleted(); void cidChanged(); - void configJsonChanged(); void uploadProgressChanged(); void uploadStatusChanged(); void manifestsChanged(); void quotaChanged(); void initCompleted(); + void initFailed(); + void natExtConfigFailed(const QString& error); + void natExtConfigCompleted(); private slots: @@ -116,7 +162,6 @@ class StorageBackend : public QObject { StorageStatus m_status; QString m_debugLogs; QString m_cid; - QString m_configJson; int m_uploadProgress = 0; QString m_uploadStatus = ""; qint64 m_uploadTotalBytes = 0; @@ -125,4 +170,5 @@ class StorageBackend : public QObject { qint64 m_quotaMaxBytes = 0; qint64 m_quotaUsedBytes = 0; qint64 m_quotaReservedBytes = 0; + QJsonDocument m_config; }; diff --git a/src/StorageUIPlugin.cpp b/src/StorageUIPlugin.cpp index 09ffcfd..622a6a3 100644 --- a/src/StorageUIPlugin.cpp +++ b/src/StorageUIPlugin.cpp @@ -54,29 +54,19 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) { root->setProperty("backend", QVariant::fromValue(static_cast(backend))); + backend->ready(); + // Storage init is done in the QML // Build config from settings if onboarding was done, otherwise use empty config - QString configJson = StorageBackend::getUserConfig(); - qDebug() << "UserConfig" << StorageBackend::getUserConfigPath(); - qDebug() << "configJson" << configJson; - // if (onboardingCompleted && !dataDir.isEmpty()) { - // configJson = backend->buildConfig(dataDir, discoveryPort, tcpPort); + // QString configJson = StorageBackend::getUserConfig(); + // qDebug() << "UserConfig" << StorageBackend::getUserConfigPath(); + // qDebug() << "configJson" << configJson; + + // LogosResult result = backend->init(configJson); + + // if (!result.success) { + // qWarning() << "StorageUIPlugin: Failed to init backend:" << result.getError(); // } - // config.json overrides everything (dev/debug use) - // QFileInfo info("config.json"); - // if (info.exists() && info.isFile()) { - // qWarning() << "StorageUIPlugin: config.json found — overriding settings config"; - // configJson = backend->buildConfigFromFile("config.json"); - // } - - // qDebug() << "StorageUIPlugin: configJson=" << configJson; - - LogosResult result = backend->init(configJson); - - if (!result.success) { - qWarning() << "StorageUIPlugin: Failed to init backend:" << result.getError(); - } - return quickWidget; } diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index e08a675..a373110 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -132,7 +132,6 @@ qt_add_qml_module(appqml StartNode.qml LogosTextField.qml LogosStorageButton.qml - Nat.qml LogosStorageLayout.qml PortForwarding.qml ) diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 01c193e..83fa598 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -25,19 +25,17 @@ Item { console.log("mock start called") } - function defaultDataDir() { - return ".cache/storage" - } - - function buildConfig() {} - function saveUserConfig() {} + function loadUserConfig() {} + function reloadIfChanged() {} - function buildUpnpConfig() {} + function enableUpnpConfig() {} - function buildNatExtConfig() {} + function enableNatExtConfig() {} + + function saveCurrentConfig() {} function stop() {} } @@ -51,6 +49,10 @@ Item { property string dataDir: "" property bool onboardingCompleted: false property string natStrategy: "any" + + Component.onCompleted: { + console.info("Settings completed") + } } StackView { @@ -63,33 +65,9 @@ Item { id: onboardingComponent OnBoarding { - backend: root.backend - // discoveryPort: settings.discoveryPort - // tcpPort: settings.tcpPort - dataDir: settings.dataDir - - onCompleted: { - // settings.discoveryPort = discoveryPort - settings.dataDir = dataDir - // settings.tcpPort = tcpPort - settings.onboardingCompleted = true - - stackView.push(natComponent) - } - } - } - - Component { - id: natComponent - - Nat { - onCompleted: function (enabled) { - if (enabled) { - settings.natStrategy = "upnp" - let config = root.backend.buildUpnpConfig(settings.dataDir) - root.backend.reloadIfChanged(config) - root.backend.start() - stackView.push(startNodeComponent) + onCompleted: function (upnpEnabled) { + if (upnpEnabled) { + root.backend.enableUpnpConfig() } else { stackView.push(portForwardingComponent) } @@ -117,6 +95,8 @@ Item { } onNext: { + settings.onboardingCompleted = true + root.backend.saveCurrentConfig() stackView.push(storageComponent) } } @@ -126,14 +106,8 @@ Item { id: portForwardingComponent PortForwarding { - onPortTcpSelected: function (port) { - settings.tcpPort = port - settings.natStrategy = "extip" - let config = root.backend.buildNatExtConfig(settings.dataDir, - port) - root.backend.reloadIfChanged(config) - root.backend.start() - stackView.push(startNodeComponent) + onCompleted: function (port) { + root.backend.enableNatExtConfig(port) } } } @@ -145,11 +119,23 @@ Item { stackView.pop() } - function onInitCompleted() { + function onInitCompleted() {} + + function onReady() { + console.info("i am ready") if (settings.onboardingCompleted) { + console.info("onboardingCompleted completed") + root.backend.loadUserConfig() root.backend.start() stackView.replace(storageComponent, StackView.Immediate) } } + + function onNatExtConfigFailed(error) {} + + function onNatExtConfigCompleted(error) { + root.backend.start() + stackView.push(startNodeComponent) + } } } diff --git a/src/qml/Nat.qml b/src/qml/Nat.qml deleted file mode 100644 index d3a9f0f..0000000 --- a/src/qml/Nat.qml +++ /dev/null @@ -1,45 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Logos.Theme -import Logos.Controls - -LogosStorageLayout { - id: root - - signal completed(bool enabled) - - ColumnLayout { - anchors.centerIn: parent - spacing: Theme.spacing.medium - width: 400 - - LogosText { - id: questionText - font.pixelSize: Theme.typography.titleText - text: "Is UPnP enabled on your router ?" - Layout.alignment: Qt.AlignCenter - } - - LogosText { - id: questionDescriptionText - font.pixelSize: Theme.typography.primaryText - text: "UPnP simplifies configuration by handling port forwarding automatically." - Layout.alignment: Qt.AlignCenter - } - - RowLayout { - spacing: Theme.spacing.medium - Layout.alignment: Qt.AlignCenter - - LogosStorageButton { - text: "No / I don't know" - onClicked: root.completed(false) - } - - LogosStorageButton { - text: "Yes, I use UPnP" - onClicked: root.completed(true) - } - } - } -} diff --git a/src/qml/OnBoarding.qml b/src/qml/OnBoarding.qml index e3fef3c..7856356 100644 --- a/src/qml/OnBoarding.qml +++ b/src/qml/OnBoarding.qml @@ -7,125 +7,169 @@ import Logos.Controls LogosStorageLayout { id: root - property int discoveryPort: 8090 - property int tcpPort: 0 - property var backend: mockBackend - property var local: false - property string dataDir: backend.defaultDataDir() - - signal completed - - QtObject { - id: mockBackend - - function defaultDataDir() { - return ".cache/storage" - } - } + signal completed(bool enabled) ColumnLayout { - anchors.centerIn: parent spacing: Theme.spacing.medium + Layout.fillWidth: true + anchors.centerIn: parent LogosText { id: titleText font.pixelSize: Theme.typography.titleText - text: "Logos Storage" + text: "Welcome to Logos Storage" Layout.alignment: Qt.AlignCenter } LogosText { id: questionText font.pixelSize: Theme.typography.titleText - text: "First, let's choose the storage folder" + text: "Is UPnP enabled on your router ?" Layout.alignment: Qt.AlignCenter } - // ColumnLayout { - // id: discoveryPortColumn - // spacing: Theme.spacing.tiny - // Layout.fillWidth: true - - // LogosText { - // text: "Discovery port" - // font.pixelSize: Theme.typography.secondaryText - // color: Theme.palette.text - // } - - // LogosTextField { - // isValid: acceptableInput && text.length > 0 - // id: discoveryPortTextField - // placeholderText: "Enter the discovery port" - // text: root.discoveryPort - // validator: IntValidator { - // bottom: 1 - // top: 65535 - // } - // onTextChanged: { - // if (isValid) { - // root.discoveryPort = parseInt(text) - // } - // } - // } - // } - ColumnLayout { - spacing: Theme.spacing.tiny - Layout.fillWidth: true - - RowLayout { - spacing: Theme.spacing.tiny - - LogosTextField { - isValid: text.trim().length > 0 - id: dataDirTextField - placeholderText: "Enter the data dir" - text: root.dataDir - Layout.fillWidth: true - onTextChanged: { - root.dataDir = text - } - } - - LogosStorageButton { - text: "Choose" - onClicked: folderDialog.open() - } - } - - FolderDialog { - id: folderDialog - onAccepted: { - dataDirTextField.text = selectedFolder - } - } + LogosText { + id: questionDescriptionText + font.pixelSize: Theme.typography.primaryText + text: "UPnP simplifies configuration by handling port forwarding automatically." + Layout.alignment: Qt.AlignCenter } - // Column { - // CheckBox { - // text: "Do you want to connect to a local network ?" - // checked: false - // onCheckedChanged: root.local = checked - // } + RowLayout { + spacing: Theme.spacing.medium + Layout.alignment: Qt.AlignCenter - // LogosText { - // font.pixelSize: Theme.typography.secondaryText - // text: "You will not " - // Layout.alignment: Qt.AlignCenter - // } - // } - } + LogosStorageButton { + text: "No / I don't know" + onClicked: root.completed(false) + } - LogosStorageButton { - text: "Next" - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.bottomMargin: 10 - anchors.rightMargin: 10 - enabled: dataDirTextField.isValid - // enabled: discoveryPortTextField.acceptableInput - // && tcpPortTextField.acceptableInput && dataDirTextField.isValid - onClicked: function () { - root.completed() + LogosStorageButton { + text: "Yes, I use UPnP" + onClicked: root.completed(true) + } } } + + // property int discoveryPort: 8090 + // property int tcpPort: 0 + // property var backend: mockBackend + // property var local: false + // property string dataDir: backend.defaultDataDir() + + // signal completed + + // QtObject { + // id: mockBackend + + // function defaultDataDir() { + // return ".cache/storage" + // } + // } + + // ColumnLayout { + // anchors.centerIn: parent + // spacing: Theme.spacing.medium + + // LogosText { + // id: titleText + // font.pixelSize: Theme.typography.titleText + // text: "Logos Storage" + // Layout.alignment: Qt.AlignCenter + // } + + // LogosText { + // id: questionText + // font.pixelSize: Theme.typography.titleText + // text: "First, let's choose the storage folder" + // Layout.alignment: Qt.AlignCenter + // } + + // // ColumnLayout { + // // id: discoveryPortColumn + // // spacing: Theme.spacing.tiny + // // Layout.fillWidth: true + + // // LogosText { + // // text: "Discovery port" + // // font.pixelSize: Theme.typography.secondaryText + // // color: Theme.palette.text + // // } + + // // LogosTextField { + // // isValid: acceptableInput && text.length > 0 + // // id: discoveryPortTextField + // // placeholderText: "Enter the discovery port" + // // text: root.discoveryPort + // // validator: IntValidator { + // // bottom: 1 + // // top: 65535 + // // } + // // onTextChanged: { + // // if (isValid) { + // // root.discoveryPort = parseInt(text) + // // } + // // } + // // } + // // } + // ColumnLayout { + // spacing: Theme.spacing.tiny + // Layout.fillWidth: true + + // RowLayout { + // spacing: Theme.spacing.tiny + + // LogosTextField { + // isValid: text.trim().length > 0 + // id: dataDirTextField + // placeholderText: "Enter the data dir" + // text: root.dataDir + // Layout.fillWidth: true + // onTextChanged: { + // root.dataDir = text + // } + // } + + // LogosStorageButton { + // text: "Choose" + // onClicked: folderDialog.open() + // } + // } + + // FolderDialog { + // id: folderDialog + // onAccepted: { + // dataDirTextField.text = selectedFolder + // } + // } + // } + + // // Column { + // // CheckBox { + // // text: "Do you want to connect to a local network ?" + // // checked: false + // // onCheckedChanged: root.local = checked + // // } + + // // LogosText { + // // font.pixelSize: Theme.typography.secondaryText + // // text: "You will not " + // // Layout.alignment: Qt.AlignCenter + // // } + // // } + // } + + // LogosStorageButton { + // text: "Next" + // anchors.bottom: parent.bottom + // anchors.right: parent.right + // anchors.bottomMargin: 10 + // anchors.rightMargin: 10 + // enabled: dataDirTextField.isValid + // // enabled: discoveryPortTextField.acceptableInput + // // && tcpPortTextField.acceptableInput && dataDirTextField.isValid + // onClicked: function () { + // root.completed() + // } + // } } diff --git a/src/qml/PortForwarding.qml b/src/qml/PortForwarding.qml index b6102a1..8e2a334 100644 --- a/src/qml/PortForwarding.qml +++ b/src/qml/PortForwarding.qml @@ -8,12 +8,13 @@ LogosStorageLayout { property var tcpPort: 0 - signal portTcpSelected(int port) + signal completed(int port) ColumnLayout { anchors.centerIn: parent spacing: Theme.spacing.medium width: 400 + Layout.fillWidth: true LogosText { id: questionText @@ -30,6 +31,7 @@ LogosStorageLayout { } LogosTextField { + Layout.fillWidth: true isValid: acceptableInput && text.length > 0 id: tcpPortTextField placeholderText: "Enter the TCP port" @@ -44,15 +46,12 @@ LogosStorageLayout { } } } - } - LogosStorageButton { - text: "Next" - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.bottomMargin: 10 - anchors.rightMargin: 10 - enabled: tcpPortTextField.isValid - onClicked: root.portTcpSelected(root.tcpPort) + LogosStorageButton { + text: "Next" + + enabled: tcpPortTextField.isValid + onClicked: root.completed(root.tcpPort) + } } } diff --git a/src/qml/StartNode.qml b/src/qml/StartNode.qml index 9beddda..2ee5499 100644 --- a/src/qml/StartNode.qml +++ b/src/qml/StartNode.qml @@ -4,13 +4,8 @@ import QtQuick.Controls import Logos.Controls import Logos.Theme -Rectangle { +LogosStorageLayout { id: root - color: Theme.palette.background - Layout.fillWidth: true - Layout.fillHeight: true - implicitWidth: 600 - implicitHeight: 400 property var backend: mockBackend property string status: "" @@ -64,9 +59,8 @@ Rectangle { } ColumnLayout { - anchors.fill: parent - anchors.margins: 20 - anchors.bottomMargin: 60 + Layout.fillWidth: true + anchors.centerIn: parent spacing: Theme.spacing.medium LogosText { diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index 0ec01ad..fff4020 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -6,5 +6,7 @@ qml/StorageView.qml qml/LogosTextField.qml qml/LogosStorageButton.qml + qml/LogosStorageLayout.qml + qml/PortForwarding.qml From 8c6bd2f4ef97779b28099cd71749a626c807d086 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 19 Feb 2026 16:47:30 +0400 Subject: [PATCH 05/13] Add error toast --- src/qml/ErrorToast.qml | 97 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/qml/ErrorToast.qml diff --git a/src/qml/ErrorToast.qml b/src/qml/ErrorToast.qml new file mode 100644 index 0000000..7b996e2 --- /dev/null +++ b/src/qml/ErrorToast.qml @@ -0,0 +1,97 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +Rectangle { + id: root + + property alias title: titleText.text + property alias message: messageText.text + + function show(t, msg) { + root.title = t + root.message = msg + root.visible = true + slideAnim.restart() + } + + function hide() { + root.visible = false + } + + visible: false + opacity: 0 + width: 500 + radius: Theme.spacing.tiny + color: "#1e1e1e" + + implicitHeight: content.implicitHeight + Theme.spacing.medium * 2 + + transform: Translate { id: slideTranslate; y: 20 } + + ParallelAnimation { + id: slideAnim + + NumberAnimation { + target: slideTranslate + property: "y" + from: 20 + to: 0 + duration: 500 + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: root + property: "opacity" + from: 0 + to: 1 + duration: 500 + easing.type: Easing.OutCubic + } + } + + ColumnLayout { + id: content + anchors { + left: parent.left + right: parent.right + verticalCenter: parent.verticalCenter + margins: Theme.spacing.medium + } + spacing: Theme.spacing.tiny + + RowLayout { + Layout.fillWidth: true + + LogosText { + id: titleText + Layout.fillWidth: true + color: Theme.palette.error + font.pixelSize: Theme.typography.primaryText + font.bold: true + } + + LogosText { + text: "✕" + color: Theme.palette.textMuted + font.pixelSize: Theme.typography.primaryText + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.hide() + } + } + } + + LogosText { + id: messageText + Layout.fillWidth: true + color: Theme.palette.text + font.pixelSize: Theme.typography.primaryText + wrapMode: Text.WordWrap + } + } +} From 733a43a4f85bcb6b767090e8c8299b378b7f9763 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 19 Feb 2026 17:44:49 +0400 Subject: [PATCH 06/13] Provide an error modal to display error --- src/StorageBackend.cpp | 57 +++++++++++++++------------ src/StorageBackend.h | 23 ++++++++++- src/StorageUIPlugin.cpp | 6 +-- src/qml/CMakeLists.txt | 1 + src/qml/ErrorToast.qml | 82 ++++++++++++++++++++++++++------------- src/qml/Main.qml | 22 +++++++++-- src/qml/StartNode.qml | 20 ++++++++++ src/qml/main.cpp | 2 +- src/storage_resources.qrc | 1 + 9 files changed, 154 insertions(+), 60 deletions(-) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index 34393e9..d534e7c 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -47,7 +47,7 @@ LogosResult StorageBackend::init(const QString& configJson) { m_config = QJsonDocument::fromJson(configJson.toUtf8()); if (m_config.isNull()) { qDebug() << "StorageBackend::initStorage invalid json config" << configJson; - emit initFailed(); + reportError("Failed to create the storage: invalid JSON config"); return {false, "", "Failed to create the storage, invalid json config"}; } @@ -57,9 +57,8 @@ LogosResult StorageBackend::init(const QString& configJson) { if (!result) { setStatus(Destroyed); - debug("Failed to init storage"); - emit initFailed(); - return {false, "", "Filed to init storage"}; + reportError("Failed to init storage"); + return {false, "", "Failed to init storage"}; } setStatus(Stopped); @@ -72,6 +71,7 @@ LogosResult StorageBackend::init(const QString& configJson) { setStatus(Stopped); debug("Failed to start Storage module:" + message); emit startFailed(message); + reportError("Failed to start: " + message); } else { setStatus(Running); debug("Storage module started."); @@ -237,6 +237,8 @@ LogosResult StorageBackend::start(const QString& newConfigJson) { setStatus(Starting); debug("Starting Storage module..."); + // TODO trach the start attempts in a file + auto result = m_logos->storage_module.start(); if (!result) { @@ -295,6 +297,11 @@ void StorageBackend::destroy() { QString StorageBackend::debugLogs() const { return m_debugLogs; }; +void StorageBackend::reportError(const QString& message) { + debug(message); + emit error(message); +} + void StorageBackend::debug(const QString& log) { if (!m_debugLogs.isEmpty()) { m_debugLogs += "\n"; @@ -866,36 +873,38 @@ void StorageBackend::enableNatExtConfig(int tcpPort) { QJsonArray listenAddrs = {QString("/ip4/0.0.0.0/tcp/%1").arg(tcpPort)}; obj["listen-addrs"] = listenAddrs; + reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact))); + qDebug() << "StorageBackend::enableNatExtConfig Retrieving the public IP"; - + emit natExtConfigCompleted(); // Create the network manager - QNetworkAccessManager* manager = new QNetworkAccessManager(this); - QNetworkRequest request(QUrl("https://echo.codex.storage/")); - request.setRawHeader("Accept", "text/plain"); - QNetworkReply* reply = manager->get(request); + // QNetworkAccessManager* manager = new QNetworkAccessManager(this); + // QNetworkRequest request(QUrl("https://echo.codex.storage/")); + // request.setRawHeader("Accept", "text/plain"); + // QNetworkReply* reply = manager->get(request); - connect(reply, &QNetworkReply::finished, this, [this, reply, manager, obj]() { - reply->deleteLater(); - manager->deleteLater(); + // connect(reply, &QNetworkReply::finished, this, [this, reply, manager, obj]() { + // reply->deleteLater(); + // manager->deleteLater(); - if (reply->error() != QNetworkReply::NoError) { - emit natExtConfigFailed(reply->errorString()); - return; - } + // if (reply->error() != QNetworkReply::NoError) { + // reportError("NAT config failed: " + reply->errorString()); + // return; + // } - QString ip = reply->readAll().trimmed(); + // QString ip = reply->readAll().trimmed(); - qDebug() << "StorageBackend::enableNatExtConfig ip=" << ip; + // qDebug() << "StorageBackend::enableNatExtConfig ip=" << ip; - obj["nat"] = "extip:" + ip; + // obj["nat"] = "extip:" + ip; - qDebug() << "StorageBackend::enableNatExtConfig config=" - << QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); + // qDebug() << "StorageBackend::enableNatExtConfig config=" + // << QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); - reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact))); + // reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact))); - emit natExtConfigCompleted(); - }); + // emit natExtConfigCompleted(); + // }); } void StorageBackend::status(StorageStatus status) { m_status = status; } diff --git a/src/StorageBackend.h b/src/StorageBackend.h index 39db7f7..8442ea1 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -133,6 +133,25 @@ class StorageBackend : public QObject { // and re-create a context with the new configuration void enableNatExtConfig(int tcpPort); + // This method try to get guidance about the resolution + // of the misconfiguration for the node. + // The idea is to check if: + // + // 1- upnp is enabled. In this case, the user should go back + // and try to configure the port forwarding + // 2- port forwarning is enabled. Indicate that the port has to + // be free and open to remote connection. + void guessResolution(); + + // This method will ensure that the node is ready to be used. + // 1- Make a call to debug function in the storage module and + // make sure that the node has peer. If not, the UI should suggest + // to modifiy the discovery port (8090) in the advance settings (to come). + // 2- Ensure that the tcp port is open to remote connection. If not, + // the UI should suggest to change go back and try another port and double + // check that the port forwarding is enabled on the router. + void checkNodeIsUp(); + signals: void ready(); void startCompleted(); @@ -146,9 +165,8 @@ class StorageBackend : public QObject { void manifestsChanged(); void quotaChanged(); void initCompleted(); - void initFailed(); - void natExtConfigFailed(const QString& error); void natExtConfigCompleted(); + void error(const QString& message); private slots: @@ -156,6 +174,7 @@ class StorageBackend : public QObject { void setStatus(StorageStatus newStatus); void peerConnect(const QString& peerId); void debug(const QString& log); + void reportError(const QString& message); LogosAPI* m_logosAPI; LogosModules* m_logos; diff --git a/src/StorageUIPlugin.cpp b/src/StorageUIPlugin.cpp index 622a6a3..0513c92 100644 --- a/src/StorageUIPlugin.cpp +++ b/src/StorageUIPlugin.cpp @@ -102,13 +102,13 @@ void StorageUIPlugin::destroyWidget(QWidget* widget) { return; } - if (backend->status() != StorageBackend::StorageStatus::Destroyed) { - qDebug() << "StorageUIPlugin::destroyWidget: backend is not initialised so let's detroy it."; + if (backend->status() == StorageBackend::StorageStatus::Destroyed) { + qDebug() << "StorageUIPlugin::destroyWidget: backend is not initialised so let's delete the widget."; quickWidget->deleteLater(); return; } - if (backend->status() == StorageBackend::StorageStatus::Running) { + if (backend->status() != StorageBackend::StorageStatus::Running) { qDebug() << "StorageUIPlugin::destroyWidget: backend is not running so let's detroy it."; backend->destroy(); diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index a373110..b7e8697 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -134,6 +134,7 @@ qt_add_qml_module(appqml LogosStorageButton.qml LogosStorageLayout.qml PortForwarding.qml + ErrorToast.qml ) # Set up QML module directory for runtime diff --git a/src/qml/ErrorToast.qml b/src/qml/ErrorToast.qml index 7b996e2..0a5c30f 100644 --- a/src/qml/ErrorToast.qml +++ b/src/qml/ErrorToast.qml @@ -9,26 +9,53 @@ Rectangle { property alias title: titleText.text property alias message: messageText.text - function show(t, msg) { - root.title = t - root.message = msg + function show(title, message) { + root.title = title + root.message = message root.visible = true slideAnim.restart() } function hide() { - root.visible = false + hideAnim.restart() } visible: false opacity: 0 width: 500 radius: Theme.spacing.tiny - color: "#1e1e1e" + color: "#3D2020" implicitHeight: content.implicitHeight + Theme.spacing.medium * 2 - transform: Translate { id: slideTranslate; y: 20 } + transform: Translate { + id: slideTranslate + y: 20 + } + + ParallelAnimation { + id: hideAnim + + NumberAnimation { + target: slideTranslate + property: "y" + from: 0 + to: 20 + duration: 300 + easing.type: Easing.InCubic + } + + NumberAnimation { + target: root + property: "opacity" + from: 1 + to: 0 + duration: 300 + easing.type: Easing.InCubic + } + + onFinished: root.visible = false + } ParallelAnimation { id: slideAnim @@ -52,6 +79,23 @@ Rectangle { } } + // Close button top right + LogosText { + text: "x" + font.pixelSize: Theme.typography.primaryText + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Theme.spacing.small + z: 1 + color: Theme.palette.text + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.hide() + } + } + ColumnLayout { id: content anchors { @@ -62,28 +106,12 @@ Rectangle { } spacing: Theme.spacing.tiny - RowLayout { + LogosText { + id: titleText Layout.fillWidth: true - - LogosText { - id: titleText - Layout.fillWidth: true - color: Theme.palette.error - font.pixelSize: Theme.typography.primaryText - font.bold: true - } - - LogosText { - text: "✕" - color: Theme.palette.textMuted - font.pixelSize: Theme.typography.primaryText - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: root.hide() - } - } + color: Theme.palette.error + font.pixelSize: Theme.typography.primaryText + font.bold: true } LogosText { diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 83fa598..bdef8bc 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtCore +import Logos.Theme // qmllint disable unqualified Item { @@ -20,6 +21,9 @@ Item { signal startFailed signal stopCompleted signal initCompleted + signal ready + signal error + signal natExtConfigCompleted function start() { console.log("mock start called") @@ -38,6 +42,8 @@ Item { function saveCurrentConfig() {} function stop() {} + + function guessResolution() {} } Settings { @@ -61,13 +67,23 @@ Item { initialItem: onboardingComponent } + ErrorToast { + id: errorToast + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.spacing.medium + } + Component { id: onboardingComponent OnBoarding { onCompleted: function (upnpEnabled) { + console.info("onboarding completed") if (upnpEnabled) { root.backend.enableUpnpConfig() + root.backend.start() + stackView.push(startNodeComponent) } else { stackView.push(portForwardingComponent) } @@ -122,16 +138,16 @@ Item { function onInitCompleted() {} function onReady() { - console.info("i am ready") if (settings.onboardingCompleted) { - console.info("onboardingCompleted completed") root.backend.loadUserConfig() root.backend.start() stackView.replace(storageComponent, StackView.Immediate) } } - function onNatExtConfigFailed(error) {} + function onError(message) { + errorToast.show("Error", message) + } function onNatExtConfigCompleted(error) { root.backend.start() diff --git a/src/qml/StartNode.qml b/src/qml/StartNode.qml index 2ee5499..c92d345 100644 --- a/src/qml/StartNode.qml +++ b/src/qml/StartNode.qml @@ -10,6 +10,7 @@ LogosStorageLayout { property var backend: mockBackend property string status: "" property string title: "Starting your node...." + property string resolution: "" property bool starting: true property bool success: false @@ -56,6 +57,8 @@ LogosStorageLayout { root.title = "Error" root.status = "Failed to start: " + error } + + function guessResolution() {} } ColumnLayout { @@ -76,6 +79,13 @@ LogosStorageLayout { text: root.status Layout.alignment: Qt.AlignHCenter } + + LogosText { + id: suggestionText + font.pixelSize: Theme.typography.primaryText + text: root.suggestion + Layout.alignment: Qt.AlignHCenter + } } LogosStorageButton { @@ -97,4 +107,14 @@ LogosStorageLayout { onClicked: root.next() enabled: root.success == true } + + Connections { + target: root.backend + + function onStartFailed(error) { + root.title = "Erreur" + root.status = "Your node failed to start with this error: " + error + root.method = root.backend.guessResolution() + } + } } diff --git a/src/qml/main.cpp b/src/qml/main.cpp index 5666f0b..6c9f90a 100644 --- a/src/qml/main.cpp +++ b/src/qml/main.cpp @@ -19,7 +19,7 @@ int main(int argc, char* argv[]) { Qt::QueuedConnection); engine.addImportPath(QCoreApplication::applicationDirPath() + "/qml"); - engine.loadFromModule("StorageBackend", "OnBoarding"); + engine.loadFromModule("StorageBackend", "Main"); return app.exec(); } diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index fff4020..a43bd09 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -8,5 +8,6 @@ qml/LogosStorageButton.qml qml/LogosStorageLayout.qml qml/PortForwarding.qml + qml/ErrorToast.qml From 0a4fcca520af8158624f02ac50dce378d8751b66 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 20 Feb 2026 13:41:07 +0400 Subject: [PATCH 07/13] Add onboarding checks --- nix/default.nix | 6 +- src/StorageBackend.cpp | 144 +++++++++++++++++++++++++++------ src/StorageBackend.h | 23 +++--- src/qml/Main.qml | 162 ++++++++++++++++++++++--------------- src/qml/OnBoarding.qml | 132 +++--------------------------- src/qml/PortForwarding.qml | 56 ++++++++++--- src/qml/StartNode.qml | 98 ++++++++++++++-------- 7 files changed, 352 insertions(+), 269 deletions(-) diff --git a/nix/default.nix b/nix/default.nix index cd277d5..b299677 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -14,9 +14,9 @@ ]; # Common runtime dependencies - buildInputs = [ - pkgs.qt6.qtbase - pkgs.qt6.qtremoteobjects + buildInputs = [ + pkgs.qt6.qtbase + pkgs.qt6.qtremoteobjects pkgs.zstd pkgs.krb5 pkgs.abseil-cpp diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index d534e7c..92c57fd 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,9 @@ StorageBackend::StorageBackend(LogosAPI* logosAPI, QObject* parent) : QObject(parent), m_status(Destroyed), m_logosAPI(nullptr), m_logos(nullptr) { qDebug() << "Initializing StorageBackend..."; + + // Disable system proxy detection — it crashes in Nix/some Linux environments + QNetworkProxyFactory::setUseSystemConfiguration(false); if (logosAPI) { m_logosAPI = logosAPI; @@ -75,8 +79,8 @@ LogosResult StorageBackend::init(const QString& configJson) { } else { setStatus(Running); debug("Storage module started."); - QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); - QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); + // QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); + // QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); emit startCompleted(); } })) { @@ -873,38 +877,128 @@ void StorageBackend::enableNatExtConfig(int tcpPort) { QJsonArray listenAddrs = {QString("/ip4/0.0.0.0/tcp/%1").arg(tcpPort)}; obj["listen-addrs"] = listenAddrs; - reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact))); + // Fetch the public IP asynchronously so we can set nat=extip:IP in the config. + // If the request fails, we proceed without the IP (node will still start, just without extip NAT). + debug("Retrieving public IP..."); - qDebug() << "StorageBackend::enableNatExtConfig Retrieving the public IP"; - emit natExtConfigCompleted(); - // Create the network manager - // QNetworkAccessManager* manager = new QNetworkAccessManager(this); - // QNetworkRequest request(QUrl("https://echo.codex.storage/")); - // request.setRawHeader("Accept", "text/plain"); - // QNetworkReply* reply = manager->get(request); + QNetworkAccessManager* manager = new QNetworkAccessManager(this); + QNetworkRequest request(ECHO_PROVIDER); - // connect(reply, &QNetworkReply::finished, this, [this, reply, manager, obj]() { - // reply->deleteLater(); - // manager->deleteLater(); + // Set text/plain to receive only the IP + request.setRawHeader("Accept", "text/plain"); - // if (reply->error() != QNetworkReply::NoError) { - // reportError("NAT config failed: " + reply->errorString()); - // return; - // } + QNetworkReply* reply = manager->get(request); - // QString ip = reply->readAll().trimmed(); + connect(reply, &QNetworkReply::finished, this, [this, reply, manager, obj]() mutable { + reply->deleteLater(); + manager->deleteLater(); - // qDebug() << "StorageBackend::enableNatExtConfig ip=" << ip; + if (reply->error() != QNetworkReply::NoError) { + debug("Failed to retrieve public IP: " + reply->errorString() + ". Proceeding without extip NAT."); + } else { + QString ip = QString::fromUtf8(reply->readAll()).trimmed(); + debug("Public IP: " + ip); + obj["nat"] = "extip:" + ip; + } - // obj["nat"] = "extip:" + ip; + reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact))); + emit natExtConfigCompleted(); + }); +} - // qDebug() << "StorageBackend::enableNatExtConfig config=" - // << QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); +void StorageBackend::checkNodeIsUp() { + qDebug() << "StorageBackend::checkNodeIsUp called."; - // reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact))); + // First we get the debug info in order to get the peers and + // the announceAddresses + LogosResult result = m_logos->storage_module.debug(); + if (!result.success) { + qDebug() << "Failed to get node debug info: " << result.getError(); + emit nodeIsntUp("Failed to get node debug info: " + result.getError()); + return; + } - // emit natExtConfigCompleted(); - // }); + // Ensure that the node has at least one peer. + QVariantMap table = result.getValue("table"); + QVariantList nodes = table["nodes"].toList(); + + debug(QString("Connected peers: %1").arg(nodes.size())); + if (nodes.isEmpty()) { + emit nodeIsntUp("No peers connected. " + "Try modifying the discovery port (default 8090) in the advanced settings."); + return; + } + + debug("DHT seems okay, found peers"); + + // Extract TCP ports from announceAddresses. + // Format: "/ip4/1.2.3.4/tcp/PORT" + QVariantList announceAddresses = result.getValue("announceAddresses"); + QList ports; + for (const QVariant& addr : announceAddresses) { + QStringList parts = addr.toString().split("/"); + // "/ip4/1.2.3.4/tcp/8079" splits to ["", "ip4", "1.2.3.4", "tcp", "8079"] + int tcpIndex = parts.indexOf("tcp"); + if (tcpIndex >= 0 && tcpIndex + 1 < parts.size()) { + int port = parts[tcpIndex + 1].toInt(); + if (port > 0 && !ports.contains(port)) { + ports.append(port); + } + } + } + + QString nat = m_config.object()["nat"].toString(); + + if (ports.isEmpty()) { + debug("No TCP ports found in announce addresses, considering node as not up"); + if (nat == "upnp") { + emit nodeIsntUp("UPnP is configured but there is no announced addresses. " + "Try going back and configure port forwarding manually on your router."); + } else { + emit nodeIsntUp("No announced addresses found. Your TCP port is propably incorrect. " + "Try going back and check your port forwarding configuration."); + } + + return; + } + + debug(QString("Checking reachability for %1 port(s)...").arg(ports.size())); + + // Check each port via the echo service, one by one. + bool foundReachable = false; + for (int port : ports) { + QNetworkAccessManager manager; + QNetworkRequest request(QUrl(QString("%1/port/%2").arg(ECHO_PROVIDER).arg(port))); + QNetworkReply* reply = manager.get(request); + + QEventLoop loop; + connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + if (reply->error() == QNetworkReply::NoError) { + bool reachable = QJsonDocument::fromJson(reply->readAll()).object()["reachable"].toBool(); + debug("Port " + QString::number(port) + (reachable ? " is reachable" : " is not reachable")); + if (reachable) { + foundReachable = true; + } + } else { + debug("Port check failed for port " + QString::number(port) + ": " + reply->errorString()); + } + + reply->deleteLater(); + } + + if (foundReachable) { + emit nodeIsUp(); + } else { + if (nat == "upnp") { + emit nodeIsntUp("UPnP is configured but the node is not reachable from the internet. " + "Try going back and configure port forwarding manually on your router."); + } else { + emit nodeIsntUp("No ports are reachable from the internet. " + "Try going back and check your port forwarding configuration."); + } + } } void StorageBackend::status(StorageStatus status) { m_status = status; } diff --git a/src/StorageBackend.h b/src/StorageBackend.h index 8442ea1..6e09a32 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -11,7 +11,7 @@ static const int RET_OK = 0; static const int RET_PROGRESS = 3; -static const QUrl ECHO_PROVIDER("https://echo.codex.storage/"); +static const QString ECHO_PROVIDER = "https://echo.codex.storage/"; static const QString APP_HOME = QDir::homePath() + "/.logos_storage"; static const QString DEFAULT_DATA_DIR = APP_HOME + "/data"; static const QString USER_CONFIG_PATH = APP_HOME + "/config.json"; @@ -133,23 +133,18 @@ class StorageBackend : public QObject { // and re-create a context with the new configuration void enableNatExtConfig(int tcpPort); - // This method try to get guidance about the resolution - // of the misconfiguration for the node. - // The idea is to check if: - // - // 1- upnp is enabled. In this case, the user should go back - // and try to configure the port forwarding - // 2- port forwarning is enabled. Indicate that the port has to - // be free and open to remote connection. - void guessResolution(); - // This method will ensure that the node is ready to be used. + // // 1- Make a call to debug function in the storage module and // make sure that the node has peer. If not, the UI should suggest // to modifiy the discovery port (8090) in the advance settings (to come). + // // 2- Ensure that the tcp port is open to remote connection. If not, // the UI should suggest to change go back and try another port and double // check that the port forwarding is enabled on the router. + // + // Emit nodeIsUp() on success + // Emit nodeIsntUp(error) on failure void checkNodeIsUp(); signals: @@ -168,6 +163,12 @@ class StorageBackend : public QObject { void natExtConfigCompleted(); void error(const QString& message); + // Emitted when the node port is reachable from the internet + void nodeIsUp(); + + // Emitted when the node port is not reachable, with a reason + void nodeIsntUp(const QString& reason); + private slots: private: diff --git a/src/qml/Main.qml b/src/qml/Main.qml index bdef8bc..e650ffa 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -4,6 +4,20 @@ import QtCore import Logos.Theme // qmllint disable unqualified + +// Application flow overview: +// On startup, the onboarding screen is shown by default. +// If the storage backend emits a ready event and onboarding is already +// complete, the onboarding screen is immediately replaced by the +// storageComponent. +// Onboarding offers two choices: +// 1. UPnP : the user proceeds directly to the startNodeComponent. +// 2. Port forwarding : the user selects a TCP port before proceeding +// to the startNodeComponent. +// The startNodeComponent waits for the node to start and verifies that +// it is reachable. If the node is unreachable, the user is prompted to +// edit the configuration. Once reachable, clicking "Next" marks +// onboarding as complete. Item { id: root implicitWidth: 800 @@ -11,54 +25,46 @@ Item { property var backend: mockBackend - QtObject { - id: mockBackend + Connections { + target: root.backend - readonly property bool isMock: true - property int status + // The node is stopped during the onboarding + // when the user try to change his settings + // and click on "Back", + // In that case, we pop the navigation after + // the node is stopped. + function onStopCompleted() { + if (!settings.onboardingCompleted) { - signal startCompleted - signal startFailed - signal stopCompleted - signal initCompleted - signal ready - signal error - signal natExtConfigCompleted - - function start() { - console.log("mock start called") + // stackView.pop() + } } - function saveUserConfig() {} + // When the onboarding is completed, + // the user should have a config save in his + // home folder. + // After the config is loaded, the node will be + // started and the storeComponent will replace + // the stackView item immediatly. + function onReady() { + if (settings.onboardingCompleted) { + root.backend.loadUserConfig() + root.backend.start() + stackView.replace(storageComponent, StackView.Immediate) + } + } - function loadUserConfig() {} - - function reloadIfChanged() {} - - function enableUpnpConfig() {} - - function enableNatExtConfig() {} - - function saveCurrentConfig() {} - - function stop() {} - - function guessResolution() {} + // If there is any error, display it in a toast view + function onError(message) { + errorToast.show("Error", message) + } } Settings { id: settings category: "Storage" - property int discoveryPort: 8090 - property int tcpPort: 0 - property string dataDir: "" property bool onboardingCompleted: false - property string natStrategy: "any" - - Component.onCompleted: { - console.info("Settings completed") - } } StackView { @@ -67,22 +73,16 @@ Item { initialItem: onboardingComponent } - ErrorToast { - id: errorToast - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - anchors.bottomMargin: Theme.spacing.medium - } - Component { id: onboardingComponent OnBoarding { + backend: root.backend + + // The completed event means the user + // selected upup or port forwarding. onCompleted: function (upnpEnabled) { - console.info("onboarding completed") if (upnpEnabled) { - root.backend.enableUpnpConfig() - root.backend.start() stackView.push(startNodeComponent) } else { stackView.push(portForwardingComponent) @@ -106,13 +106,11 @@ Item { backend: root.backend onBack: { - root.backend.stop() stackView.pop() } onNext: { settings.onboardingCompleted = true - root.backend.saveCurrentConfig() stackView.push(storageComponent) } } @@ -122,36 +120,66 @@ Item { id: portForwardingComponent PortForwarding { - onCompleted: function (port) { - root.backend.enableNatExtConfig(port) + backend: root.backend + loading: false + + onBack: { + stackView.pop() + } + + onCompleted: function () { + stackView.push(startNodeComponent) } } } - Connections { - target: root.backend + ErrorToast { + id: errorToast + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Theme.spacing.medium + } - function onStopCompleted() { - stackView.pop() + QtObject { + id: mockBackend + + readonly property bool isMock: true + property int status + + signal startCompleted + signal startFailed + signal stopCompleted + signal initCompleted + signal ready + signal error + signal natExtConfigCompleted + signal nodeIsUp + signal nodeIsntUp + + function start() { + console.log("mock start called") } - function onInitCompleted() {} + function saveUserConfig() {} - function onReady() { - if (settings.onboardingCompleted) { - root.backend.loadUserConfig() - root.backend.start() - stackView.replace(storageComponent, StackView.Immediate) - } + function loadUserConfig() {} + + function reloadIfChanged() {} + + function enableUpnpConfig() {} + + function enableNatExtConfig() { + natExtConfigCompleted() } - function onError(message) { - errorToast.show("Error", message) - } + function saveCurrentConfig() {} - function onNatExtConfigCompleted(error) { - root.backend.start() - stackView.push(startNodeComponent) + function stop() {} + + function checkNodeIsUp() {} + + function guessResolution() { + return "" } } } diff --git a/src/qml/OnBoarding.qml b/src/qml/OnBoarding.qml index 7856356..77a03f6 100644 --- a/src/qml/OnBoarding.qml +++ b/src/qml/OnBoarding.qml @@ -7,6 +7,8 @@ import Logos.Controls LogosStorageLayout { id: root + property var backend: mockBackend + signal completed(bool enabled) ColumnLayout { @@ -46,130 +48,18 @@ LogosStorageLayout { LogosStorageButton { text: "Yes, I use UPnP" - onClicked: root.completed(true) + onClicked: function () { + console.info("enableUpnpConfig") + root.backend.enableUpnpConfig() + root.completed(true) + } } } } - // property int discoveryPort: 8090 - // property int tcpPort: 0 - // property var backend: mockBackend - // property var local: false - // property string dataDir: backend.defaultDataDir() + QtObject { + id: mockBackend - // signal completed - - // QtObject { - // id: mockBackend - - // function defaultDataDir() { - // return ".cache/storage" - // } - // } - - // ColumnLayout { - // anchors.centerIn: parent - // spacing: Theme.spacing.medium - - // LogosText { - // id: titleText - // font.pixelSize: Theme.typography.titleText - // text: "Logos Storage" - // Layout.alignment: Qt.AlignCenter - // } - - // LogosText { - // id: questionText - // font.pixelSize: Theme.typography.titleText - // text: "First, let's choose the storage folder" - // Layout.alignment: Qt.AlignCenter - // } - - // // ColumnLayout { - // // id: discoveryPortColumn - // // spacing: Theme.spacing.tiny - // // Layout.fillWidth: true - - // // LogosText { - // // text: "Discovery port" - // // font.pixelSize: Theme.typography.secondaryText - // // color: Theme.palette.text - // // } - - // // LogosTextField { - // // isValid: acceptableInput && text.length > 0 - // // id: discoveryPortTextField - // // placeholderText: "Enter the discovery port" - // // text: root.discoveryPort - // // validator: IntValidator { - // // bottom: 1 - // // top: 65535 - // // } - // // onTextChanged: { - // // if (isValid) { - // // root.discoveryPort = parseInt(text) - // // } - // // } - // // } - // // } - // ColumnLayout { - // spacing: Theme.spacing.tiny - // Layout.fillWidth: true - - // RowLayout { - // spacing: Theme.spacing.tiny - - // LogosTextField { - // isValid: text.trim().length > 0 - // id: dataDirTextField - // placeholderText: "Enter the data dir" - // text: root.dataDir - // Layout.fillWidth: true - // onTextChanged: { - // root.dataDir = text - // } - // } - - // LogosStorageButton { - // text: "Choose" - // onClicked: folderDialog.open() - // } - // } - - // FolderDialog { - // id: folderDialog - // onAccepted: { - // dataDirTextField.text = selectedFolder - // } - // } - // } - - // // Column { - // // CheckBox { - // // text: "Do you want to connect to a local network ?" - // // checked: false - // // onCheckedChanged: root.local = checked - // // } - - // // LogosText { - // // font.pixelSize: Theme.typography.secondaryText - // // text: "You will not " - // // Layout.alignment: Qt.AlignCenter - // // } - // // } - // } - - // LogosStorageButton { - // text: "Next" - // anchors.bottom: parent.bottom - // anchors.right: parent.right - // anchors.bottomMargin: 10 - // anchors.rightMargin: 10 - // enabled: dataDirTextField.isValid - // // enabled: discoveryPortTextField.acceptableInput - // // && tcpPortTextField.acceptableInput && dataDirTextField.isValid - // onClicked: function () { - // root.completed() - // } - // } + function enableUpnpConfig() {} + } } diff --git a/src/qml/PortForwarding.qml b/src/qml/PortForwarding.qml index 8e2a334..7698476 100644 --- a/src/qml/PortForwarding.qml +++ b/src/qml/PortForwarding.qml @@ -7,9 +7,25 @@ LogosStorageLayout { id: root property var tcpPort: 0 + property bool loading: false + property var backend: mockBackend + signal back signal completed(int port) + Connections { + target: root.backend + + // The nat ext checking needs a bit of + // time because the Storage backend retrieves + // the public IP by making a call to the echo service. + // When the config is done, just push the startNodeComponent. + function onNatExtConfigCompleted() { + root.loading = false + root.completed(root.tcpPort) + } + } + ColumnLayout { anchors.centerIn: parent spacing: Theme.spacing.medium @@ -32,26 +48,46 @@ LogosStorageLayout { LogosTextField { Layout.fillWidth: true - isValid: acceptableInput && text.length > 0 id: tcpPortTextField placeholderText: "Enter the TCP port" text: root.tcpPort - validator: IntValidator { - bottom: 0 - top: 65536 + enabled: !root.loading + isValid: { + const val = parseInt(text) + return !isNaN(val) && val >= 0 && val <= 65535 } onTextChanged: { - if (isValid) { - root.tcpPort = parseInt(text) + const val = parseInt(text) + if (!isNaN(val) && val >= 0 && val <= 65535) { + root.tcpPort = val } } } - LogosStorageButton { - text: "Next" + Row { + spacing: Theme.spacing.small - enabled: tcpPortTextField.isValid - onClicked: root.completed(root.tcpPort) + LogosStorageButton { + text: "Back" + onClicked: { + root.back() + } + } + + LogosStorageButton { + text: "Next" + enabled: !root.loading && tcpPortTextField.isValid + onClicked: { + root.loading = true + root.backend.enableNatExtConfig(root.tcpPort) + } + } } } + + QtObject { + id: mockBackend + + function enableNatExtConfig(port) {} + } } diff --git a/src/qml/StartNode.qml b/src/qml/StartNode.qml index c92d345..a52480b 100644 --- a/src/qml/StartNode.qml +++ b/src/qml/StartNode.qml @@ -24,46 +24,48 @@ LogosStorageLayout { root.success = true } - QtObject { - id: mockBackend - - readonly property bool isMock: true - property string configJson: "{}" - - signal startCompleted - signal startFailed - signal nodeStarted - } + Component.onCompleted: root.backend.start() + // Wait after startCompleted before calling checkNodeIs to + // make sure the the node is started and ready. Timer { - interval: 2000 - running: root.backend && root.backend.isMock === true - onTriggered: { - console.log("timer triggered") - root.onNodeStarted() - } + id: nodeCheckTimer + interval: 500 + repeat: false + onTriggered: root.backend.checkNodeIsUp() } Connections { target: root.backend function onStartCompleted() { - console.log("onStartCompleted") - root.onNodeStarted() + console.info("startCompleted") + root.title = "Checking.." + root.status = "Your node is started, checking everything is up." + nodeCheckTimer.start() } function onStartFailed(error) { root.starting = false - root.title = "Error" - root.status = "Failed to start: " + error + root.title = "Failed to start" + root.status = "Your node failed to start: " + error } - function guessResolution() {} + function onNodeIsUp() { + root.onNodeStarted() + } + + function onNodeIsntUp(reason) { + root.starting = false + root.title = "Node not reachable" + root.status = "" + root.resolution = reason + } } ColumnLayout { - Layout.fillWidth: true anchors.centerIn: parent + width: 400 spacing: Theme.spacing.medium LogosText { @@ -78,12 +80,17 @@ LogosStorageLayout { font.pixelSize: Theme.typography.primaryText text: root.status Layout.alignment: Qt.AlignHCenter + wrapMode: Text.WordWrap } LogosText { - id: suggestionText + id: resolutionText font.pixelSize: Theme.typography.primaryText - text: root.suggestion + text: root.resolution + visible: root.resolution !== "" + color: Theme.palette.error + wrapMode: Text.WordWrap + Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter } } @@ -94,7 +101,11 @@ LogosStorageLayout { anchors.bottomMargin: 10 anchors.leftMargin: 10 text: "Back" - onClicked: root.back() + onClicked: function () { + root.backend.stop() + root.back() + } + enabled: root.starting == false } @@ -104,17 +115,40 @@ LogosStorageLayout { anchors.bottomMargin: 10 anchors.rightMargin: 10 text: "Next" - onClicked: root.next() + onClicked: function () { + root.backend.saveCurrentConfig() + root.next() + } enabled: root.success == true } - Connections { - target: root.backend + // In preview/mock mode, simulate a successful node start after 2 seconds + Timer { + interval: 2000 + running: root.backend && root.backend.isMock === true + onTriggered: root.onNodeStarted() + repeat: false + } - function onStartFailed(error) { - root.title = "Erreur" - root.status = "Your node failed to start with this error: " + error - root.method = root.backend.guessResolution() + QtObject { + id: mockBackend + + readonly property bool isMock: true + property string configJson: "{}" + + signal startCompleted + signal startFailed(string error) + signal nodeIsUp + signal nodeIsntUp(string reason) + + function guessResolution() { + return "" } + + function checkNodeIsUp() {} + + function stop() {} + + function saveCurrentConfig() {} } } From e2be44cc0884b37bf93552e276554d2a65b40906 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 20 Feb 2026 14:23:26 +0400 Subject: [PATCH 08/13] Add Health indicator --- src/qml/CMakeLists.txt | 1 + src/qml/HealthIndicator.qml | 78 +++++++++++++++++++++++++++++++++++++ src/qml/StorageView.qml | 12 +++--- src/storage_resources.qrc | 1 + 4 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 src/qml/HealthIndicator.qml diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index b7e8697..8850f6d 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -135,6 +135,7 @@ qt_add_qml_module(appqml LogosStorageLayout.qml PortForwarding.qml ErrorToast.qml + HealthIndicator.qml ) # Set up QML module directory for runtime diff --git a/src/qml/HealthIndicator.qml b/src/qml/HealthIndicator.qml new file mode 100644 index 0000000..74a7aa2 --- /dev/null +++ b/src/qml/HealthIndicator.qml @@ -0,0 +1,78 @@ +import QtQuick + +Item { + id: root + + property bool nodeIsUp: false + property var backend: mockBackend + readonly property int running: 2 + + Timer { + readonly property int threeMinutes: 180000 + + interval: threeMinutes + repeat: true + running: root.backend.status == root.running + triggeredOnStart: true + onTriggered: root.backend.checkNodeIsUp() + } + + Connections { + target: root.backend + + function onNodeIsUp() { + root.nodeIsUp = true + } + + function onNodeIsntUp(reason) { + root.nodeIsUp = false + } + + function onStatusChanged() { + if (root.backend.status !== root.running) { + root.nodeIsUp = false + } + } + } + + property bool blinkOn: true + + Timer { + interval: 600 + repeat: true + running: true + onTriggered: root.blinkOn = !root.blinkOn + } + + Row { + id: nodeStatusBadge + anchors.top: parent.top + anchors.right: parent.right + anchors.topMargin: 18 + anchors.rightMargin: 20 + spacing: 7 + + Rectangle { + width: 10 + height: 10 + radius: 5 + anchors.verticalCenter: parent.verticalCenter + color: root.nodeIsUp ? "#4CAF50" : "#f44336" + opacity: root.blinkOn ? 1.0 : 0.15 + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: root.nodeIsUp ? "Node reachable" : "Node unreachable" + color: root.nodeIsUp ? "#4CAF50" : "#f44336" + font.pixelSize: 12 + } + } + + QtObject { + id: mockBackend + + signal nodeIsUp + signal nodeIsntUp(string reason) + } +} diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index 4e45207..d18e5dc 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -5,13 +5,8 @@ import QtQuick.Layouts import QtCore // qmllint disable unqualified -Rectangle { +LogosStorageLayout { id: root - Layout.fillWidth: true - Layout.fillHeight: true - implicitWidth: 800 - implicitHeight: 800 - color: "#000000" property var backend: mockBackend readonly property int stopped: 0 @@ -92,6 +87,11 @@ Rectangle { return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" } + HealthIndicator { + backend: root.backend + anchors.fill: parent + } + Text { id: statusTextElement objectName: "status" diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index a43bd09..fe9cd5e 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -9,5 +9,6 @@ qml/LogosStorageLayout.qml qml/PortForwarding.qml qml/ErrorToast.qml + qml/HealthIndicator.qml From 16d16de3c794a5067ac7ba8892b365d22d753413 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 20 Feb 2026 14:35:52 +0400 Subject: [PATCH 09/13] Add more qml --- src/qml/AdvancedSetup.qml | 110 +++++++++++++++++++++++ src/qml/ModeSelector.qml | 179 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 src/qml/AdvancedSetup.qml create mode 100644 src/qml/ModeSelector.qml diff --git a/src/qml/AdvancedSetup.qml b/src/qml/AdvancedSetup.qml new file mode 100644 index 0000000..48dd6fc --- /dev/null +++ b/src/qml/AdvancedSetup.qml @@ -0,0 +1,110 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +LogosStorageLayout { + id: root + + property var backend: null + + signal back + signal completed(string configJson) + + ColumnLayout { + anchors.fill: parent + anchors.margins: 40 + spacing: Theme.spacing.medium + + LogosText { + text: "Advanced Configuration" + font.pixelSize: Theme.typography.titleText + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: "Edit the JSON configuration below, then click Validate." + font.pixelSize: Theme.typography.primaryText + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // ── JSON editor ────────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "#1e1e1e" + radius: 8 + border.color: jsonArea.isValid ? "#3a3a3a" : "#ff0000" + border.width: 1 + + ScrollView { + anchors.fill: parent + anchors.margins: 2 + + TextArea { + id: jsonArea + font.family: "monospace" + font.pixelSize: 12 + color: "#d4d4d4" + wrapMode: Text.WrapAnywhere + background: Item {} + + property bool isValid: true + + Component.onCompleted: { + text = (root.backend && root.backend.configJson) ? root.backend.configJson : "{}" + validate() + } + + function validate() { + try { + JSON.parse(text) + isValid = true + } catch (e) { + isValid = false + } + } + + onTextChanged: validate() + } + } + } + + // ── Buttons ────────────────────────────────────────────────────── + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Theme.spacing.medium + + LogosStorageButton { + text: "Back" + onClicked: root.back() + } + + Rectangle { + width: 120 + height: 36 + radius: 8 + color: jsonArea.isValid ? "#4CAF50" : "#444444" + + Text { + anchors.centerIn: parent + text: "Validate" + color: "white" + font.pixelSize: 14 + font.bold: true + } + + MouseArea { + anchors.fill: parent + enabled: jsonArea.isValid + cursorShape: jsonArea.isValid ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: root.completed(jsonArea.text) + } + } + } + } +} diff --git a/src/qml/ModeSelector.qml b/src/qml/ModeSelector.qml new file mode 100644 index 0000000..a9861b3 --- /dev/null +++ b/src/qml/ModeSelector.qml @@ -0,0 +1,179 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +LogosStorageLayout { + id: root + + signal completed(bool isGuide) + + property int selectedMode: -1 // 0 = guide, 1 = advanced + + ColumnLayout { + anchors.centerIn: parent + spacing: Theme.spacing.medium + width: 430 + + LogosText { + text: "Logos Storage" + font.pixelSize: Theme.typography.titleText + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: "How would you like to set up your node?" + font.pixelSize: Theme.typography.primaryText + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Item { + height: Theme.spacing.medium + } + + Row { + spacing: Theme.spacing.medium + Layout.alignment: Qt.AlignHCenter + + // ── Guide card ─────────────────────────────────────────────── + Rectangle { + width: 190 + height: 230 + radius: 14 + color: root.selectedMode === 0 ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + border.color: root.selectedMode === 0 ? "white" : Qt.rgba(1, 1, 1, 0.2) + border.width: root.selectedMode === 0 ? 2 : 1 + + ColumnLayout { + anchors.centerIn: parent + spacing: 14 + + // Nothing OS dot icon — crosshair/compass + Grid { + columns: 5 + spacing: 4 + Layout.alignment: Qt.AlignHCenter + + Repeater { + model: [ + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 1, 1, 0, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0 + ] + Rectangle { + width: 6 + height: 6 + radius: 2 + color: "white" + opacity: modelData ? 0.9 : 0.1 + } + } + } + + Text { + text: "Guide" + color: "white" + font.pixelSize: 16 + font.bold: true + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: "Step-by-step setup.\nRecommended for\nmost users." + color: Qt.rgba(1, 1, 1, 0.55) + font.pixelSize: 12 + horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 150 + wrapMode: Text.WordWrap + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.selectedMode = 0 + } + } + + // ── Advanced card ──────────────────────────────────────────── + Rectangle { + width: 190 + height: 230 + radius: 14 + color: root.selectedMode === 1 ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + border.color: root.selectedMode === 1 ? "white" : Qt.rgba(1, 1, 1, 0.2) + border.width: root.selectedMode === 1 ? 2 : 1 + + ColumnLayout { + anchors.centerIn: parent + spacing: 14 + + // Nothing OS dot icon — X pattern + Grid { + columns: 5 + spacing: 4 + Layout.alignment: Qt.AlignHCenter + + Repeater { + model: [ + 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, + 1, 0, 0, 0, 1 + ] + Rectangle { + width: 6 + height: 6 + radius: 2 + color: "white" + opacity: modelData ? 0.9 : 0.1 + } + } + } + + Text { + text: "Advanced" + color: "white" + font.pixelSize: 16 + font.bold: true + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: "Manual JSON\nconfiguration for\nexperienced users." + color: Qt.rgba(1, 1, 1, 0.55) + font.pixelSize: 12 + horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 150 + wrapMode: Text.WordWrap + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.selectedMode = 1 + } + } + } + + Item { + height: Theme.spacing.small + } + + LogosStorageButton { + text: "Continue" + enabled: root.selectedMode !== -1 + onClicked: root.completed(root.selectedMode === 0) + Layout.alignment: Qt.AlignHCenter + } + } +} From a7d1e092a3355e5918fef1996eb044534de1cb39 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 20 Feb 2026 15:07:58 +0400 Subject: [PATCH 10/13] Add nice onboarding --- src/StorageBackend.cpp | 8 +- src/StorageBackend.h | 1 - src/qml/AdvancedSetup.qml | 11 ++- src/qml/CMakeLists.txt | 2 + src/qml/Main.qml | 46 ++++++++-- src/qml/ModeSelector.qml | 34 +++---- src/qml/OnBoarding.qml | 187 +++++++++++++++++++++++++++++++++----- src/qml/StorageView.qml | 2 + 8 files changed, 228 insertions(+), 63 deletions(-) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index 92c57fd..494e0dc 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -772,8 +772,6 @@ StorageBackend::StorageStatus StorageBackend::status() const { return m_status; QString StorageBackend::cid() const { return m_cid; } -QString StorageBackend::configJson() const { return QString::fromUtf8(m_config.toJson(QJsonDocument::Compact)); } - int StorageBackend::uploadProgress() const { return m_uploadProgress; } QString StorageBackend::uploadStatus() const { return m_uploadStatus; } @@ -824,7 +822,7 @@ void StorageBackend::reloadIfChanged(const QString& configJson) { void StorageBackend::saveCurrentConfig() { qDebug() << "StorageBackend::saveUserConfig"; - saveUserConfig(QString::fromUtf8(m_config.toJson(QJsonDocument::Compact))); + saveUserConfig(QString::fromUtf8(m_config.toJson(QJsonDocument::Indented))); } void StorageBackend::saveUserConfig(const QString& configJson) { @@ -865,7 +863,7 @@ void StorageBackend::enableUpnpConfig() { obj["nat"] = "upnp"; - reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact))); + reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Indented))); } void StorageBackend::enableNatExtConfig(int tcpPort) { @@ -1013,7 +1011,7 @@ void StorageBackend::loadUserConfig() { result = init(QString::fromUtf8(file.readAll())); } else { qWarning() << "StorageBackend::loadUserConfig Failed to read the user config file, fallback to default config"; - result = init(QString::fromUtf8(defaultConfig().toJson(QJsonDocument::Compact))); + result = init(QString::fromUtf8(defaultConfig().toJson(QJsonDocument::Indented))); } if (!result.success) { diff --git a/src/StorageBackend.h b/src/StorageBackend.h index 6e09a32..06f8eab 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -62,7 +62,6 @@ class StorageBackend : public QObject { QString cid() const; QString debugLogs() const; StorageStatus status() const; - QString configJson() const; int uploadProgress() const; QString uploadStatus() const; QVariantList manifests() const; diff --git a/src/qml/AdvancedSetup.qml b/src/qml/AdvancedSetup.qml index 48dd6fc..e53bda8 100644 --- a/src/qml/AdvancedSetup.qml +++ b/src/qml/AdvancedSetup.qml @@ -10,7 +10,7 @@ LogosStorageLayout { property var backend: null signal back - signal completed(string configJson) + signal completed ColumnLayout { anchors.fill: parent @@ -56,7 +56,8 @@ LogosStorageLayout { property bool isValid: true Component.onCompleted: { - text = (root.backend && root.backend.configJson) ? root.backend.configJson : "{}" + text = (root.backend + && root.backend.configJson) ? root.backend.configJson : "{}" validate() } @@ -102,7 +103,11 @@ LogosStorageLayout { anchors.fill: parent enabled: jsonArea.isValid cursorShape: jsonArea.isValid ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: root.completed(jsonArea.text) + onClicked: function () { + root.backend.saveUserConfig(jsonArea.text) + root.backend.reloadIfChanged(jsonArea.text) + root.completed() + } } } } diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index 8850f6d..1e360ef 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -136,6 +136,8 @@ qt_add_qml_module(appqml PortForwarding.qml ErrorToast.qml HealthIndicator.qml + ModeSelector.qml + AdvancedSetup.qml ) # Set up QML module directory for runtime diff --git a/src/qml/Main.qml b/src/qml/Main.qml index e650ffa..a5c0a31 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -33,12 +33,12 @@ Item { // and click on "Back", // In that case, we pop the navigation after // the node is stopped. - function onStopCompleted() { - if (!settings.onboardingCompleted) { + // function onStopCompleted() { + // if (!settings.onboardingCompleted) { - // stackView.pop() - } - } + // // stackView.pop() + // } + // } // When the onboarding is completed, // the user should have a config save in his @@ -49,7 +49,6 @@ Item { function onReady() { if (settings.onboardingCompleted) { root.backend.loadUserConfig() - root.backend.start() stackView.replace(storageComponent, StackView.Immediate) } } @@ -70,7 +69,21 @@ Item { StackView { id: stackView anchors.fill: parent - initialItem: onboardingComponent + initialItem: modeSelectorComponent + } + + Component { + id: modeSelectorComponent + + ModeSelector { + onCompleted: function (isGuide) { + if (isGuide) { + stackView.push(onboardingComponent) + } else { + stackView.push(advancedSetupComponent) + } + } + } } Component { @@ -79,8 +92,8 @@ Item { OnBoarding { backend: root.backend - // The completed event means the user - // selected upup or port forwarding. + onBack: stackView.pop() + onCompleted: function (upnpEnabled) { if (upnpEnabled) { stackView.push(startNodeComponent) @@ -91,6 +104,21 @@ Item { } } + Component { + id: advancedSetupComponent + + AdvancedSetup { + backend: root.backend + + onBack: stackView.pop() + + onCompleted: function () { + settings.onboardingCompleted = true + stackView.replace(storageComponent, StackView.Immediate) + } + } + } + Component { id: storageComponent diff --git a/src/qml/ModeSelector.qml b/src/qml/ModeSelector.qml index a9861b3..ffd14d7 100644 --- a/src/qml/ModeSelector.qml +++ b/src/qml/ModeSelector.qml @@ -43,28 +43,25 @@ LogosStorageLayout { width: 190 height: 230 radius: 14 - color: root.selectedMode === 0 ? Qt.rgba(1, 1, 1, 0.08) : "transparent" - border.color: root.selectedMode === 0 ? "white" : Qt.rgba(1, 1, 1, 0.2) + color: root.selectedMode === 0 ? Qt.rgba(1, 1, 1, + 0.08) : "transparent" + border.color: root.selectedMode === 0 ? "white" : Qt.rgba(1, 1, + 1, + 0.2) border.width: root.selectedMode === 0 ? 2 : 1 ColumnLayout { anchors.centerIn: parent spacing: 14 - // Nothing OS dot icon — crosshair/compass + // Nothing OS dot icon like Grid { columns: 5 spacing: 4 Layout.alignment: Qt.AlignHCenter Repeater { - model: [ - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0, - 1, 1, 0, 1, 1, - 0, 0, 1, 0, 0, - 0, 0, 1, 0, 0 - ] + model: [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0] Rectangle { width: 6 height: 6 @@ -106,28 +103,25 @@ LogosStorageLayout { width: 190 height: 230 radius: 14 - color: root.selectedMode === 1 ? Qt.rgba(1, 1, 1, 0.08) : "transparent" - border.color: root.selectedMode === 1 ? "white" : Qt.rgba(1, 1, 1, 0.2) + color: root.selectedMode === 1 ? Qt.rgba(1, 1, 1, + 0.08) : "transparent" + border.color: root.selectedMode === 1 ? "white" : Qt.rgba(1, 1, + 1, + 0.2) border.width: root.selectedMode === 1 ? 2 : 1 ColumnLayout { anchors.centerIn: parent spacing: 14 - // Nothing OS dot icon — X pattern + // Nothing OS dot icon like Grid { columns: 5 spacing: 4 Layout.alignment: Qt.AlignHCenter Repeater { - model: [ - 1, 0, 0, 0, 1, - 0, 1, 0, 1, 0, - 0, 0, 1, 0, 0, - 0, 1, 0, 1, 0, - 1, 0, 0, 0, 1 - ] + model: [1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1] Rectangle { width: 6 height: 6 diff --git a/src/qml/OnBoarding.qml b/src/qml/OnBoarding.qml index 77a03f6..e3d8488 100644 --- a/src/qml/OnBoarding.qml +++ b/src/qml/OnBoarding.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Dialogs import QtQuick.Layouts import Logos.Theme import Logos.Controls @@ -9,49 +8,187 @@ LogosStorageLayout { property var backend: mockBackend - signal completed(bool enabled) + signal back + signal completed(bool upnpEnabled) + + property int selectedMode: -1 // 0 = upnp, 1 = port forwarding ColumnLayout { - spacing: Theme.spacing.medium - Layout.fillWidth: true anchors.centerIn: parent + spacing: Theme.spacing.medium + width: 430 LogosText { - id: titleText + text: "Network Configuration" font.pixelSize: Theme.typography.titleText - text: "Welcome to Logos Storage" - Layout.alignment: Qt.AlignCenter + Layout.alignment: Qt.AlignHCenter } LogosText { - id: questionText - font.pixelSize: Theme.typography.titleText - text: "Is UPnP enabled on your router ?" - Layout.alignment: Qt.AlignCenter - } - - LogosText { - id: questionDescriptionText + text: "How is your network configured?" font.pixelSize: Theme.typography.primaryText - text: "UPnP simplifies configuration by handling port forwarding automatically." - Layout.alignment: Qt.AlignCenter + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Item { + height: Theme.spacing.medium + } + + Row { + spacing: Theme.spacing.medium + Layout.alignment: Qt.AlignHCenter + + // ── UPnP card ──────────────────────────────────────────────── + Rectangle { + width: 190 + height: 230 + radius: 14 + color: root.selectedMode === 0 ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + border.color: root.selectedMode === 0 ? "white" : Qt.rgba(1, 1, 1, 0.2) + border.width: root.selectedMode === 0 ? 2 : 1 + + ColumnLayout { + anchors.centerIn: parent + spacing: 14 + + // Nothing OS dot icon — diamond/network + Grid { + columns: 5 + spacing: 4 + Layout.alignment: Qt.AlignHCenter + + Repeater { + model: [ + 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0 + ] + Rectangle { + width: 6 + height: 6 + radius: 2 + color: "white" + opacity: modelData ? 0.9 : 0.1 + } + } + } + + Text { + text: "UPnP" + color: "white" + font.pixelSize: 16 + font.bold: true + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: "Automatic port\nforwarding via\nUPnP router." + color: Qt.rgba(1, 1, 1, 0.55) + font.pixelSize: 12 + horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 150 + wrapMode: Text.WordWrap + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.selectedMode = 0 + } + } + + // ── Port Forwarding card ───────────────────────────────────── + Rectangle { + width: 190 + height: 230 + radius: 14 + color: root.selectedMode === 1 ? Qt.rgba(1, 1, 1, 0.08) : "transparent" + border.color: root.selectedMode === 1 ? "white" : Qt.rgba(1, 1, 1, 0.2) + border.width: root.selectedMode === 1 ? 2 : 1 + + ColumnLayout { + anchors.centerIn: parent + spacing: 14 + + // Nothing OS dot icon — right arrow + Grid { + columns: 5 + spacing: 4 + Layout.alignment: Qt.AlignHCenter + + Repeater { + model: [ + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 0, + 0, 0, 1, 0, 0 + ] + Rectangle { + width: 6 + height: 6 + radius: 2 + color: "white" + opacity: modelData ? 0.9 : 0.1 + } + } + } + + Text { + text: "Port Forwarding" + color: "white" + font.pixelSize: 16 + font.bold: true + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: "Manual TCP port\nconfiguration on\nyour router." + color: Qt.rgba(1, 1, 1, 0.55) + font.pixelSize: 12 + horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 150 + wrapMode: Text.WordWrap + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.selectedMode = 1 + } + } + } + + Item { + height: Theme.spacing.small } RowLayout { + Layout.alignment: Qt.AlignHCenter spacing: Theme.spacing.medium - Layout.alignment: Qt.AlignCenter LogosStorageButton { - text: "No / I don't know" - onClicked: root.completed(false) + text: "Back" + onClicked: root.back() } LogosStorageButton { - text: "Yes, I use UPnP" - onClicked: function () { - console.info("enableUpnpConfig") - root.backend.enableUpnpConfig() - root.completed(true) + text: "Continue" + enabled: root.selectedMode !== -1 + onClicked: { + if (root.selectedMode === 0) { + root.backend.enableUpnpConfig() + } + root.completed(root.selectedMode === 0) } } } diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index d18e5dc..78a55a0 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -53,6 +53,8 @@ LogosStorageLayout { return backend.status == running } + Component.onCompleted: root.backend.start() + QtObject { id: mockBackend From 7907ecc6b2b29332462139dbf2376c75d8ccc2ec Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 20 Feb 2026 15:30:47 +0400 Subject: [PATCH 11/13] Add files to git --- src/qml/DotIcon.qml | 74 ++++++++++++++++++++++++++++++++++++++ src/qml/NodeStatusIcon.qml | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 src/qml/DotIcon.qml create mode 100644 src/qml/NodeStatusIcon.qml diff --git a/src/qml/DotIcon.qml b/src/qml/DotIcon.qml new file mode 100644 index 0000000..de2a32a --- /dev/null +++ b/src/qml/DotIcon.qml @@ -0,0 +1,74 @@ +import QtQuick + +// Generic Nothing OS style dot grid icon. +// Static mode : set `pattern` (flat array of 0/1) and leave `animated: false` +// Animated mode: set `animated: true` — wave expands from center automatically +Item { + id: root + + // Static pattern — flat array of 0/1, row-major + property var pattern: [] + + // Dimensions + property int columns: 5 + property int dotSize: 6 + property int dotSpacing: 4 + + // Appearance + property color dotColor: "white" + property real inactiveOpacity: 0.1 + property real activeOpacity: 0.9 + + // Animation + property bool animated: false + property int animPhase: 0 + + readonly property int rows: Math.max(1, Math.ceil(pattern.length / columns)) + readonly property int count: animated ? columns * columns : pattern.length + + implicitWidth: columns * dotSize + Math.max(0, columns - 1) * dotSpacing + implicitHeight: rows * dotSize + Math.max(0, rows - 1) * dotSpacing + width: implicitWidth + height: implicitHeight + + Timer { + interval: 140 + repeat: true + running: root.animated + onTriggered: root.animPhase = (root.animPhase + 1) % (root.columns * 2) + } + + Grid { + columns: root.columns + spacing: root.dotSpacing + + Repeater { + model: root.count + + Rectangle { + width: root.dotSize + height: root.dotSize + radius: root.dotSize * 0.3 + color: root.dotColor + + opacity: { + if (!root.animated) { + return (index < root.pattern.length && root.pattern[index]) + ? root.activeOpacity : root.inactiveOpacity + } + // Wave from center + const cx = Math.floor(root.columns / 2) + const cy = Math.floor(root.columns / 2) + const col = index % root.columns + const row = Math.floor(index / root.columns) + const d = Math.abs(col - cx) + Math.abs(row - cy) + const wave = root.animPhase % root.columns + const diff = Math.abs(d - wave) + if (diff === 0) return root.activeOpacity + if (diff === 1) return 0.35 + return root.inactiveOpacity + } + } + } + } +} diff --git a/src/qml/NodeStatusIcon.qml b/src/qml/NodeStatusIcon.qml new file mode 100644 index 0000000..8c42999 --- /dev/null +++ b/src/qml/NodeStatusIcon.qml @@ -0,0 +1,71 @@ +import QtQuick +import Logos.Theme + +// 7x7 animated dot grid for the StartNode screen. +// States: +// starting=true → white wave expanding from center +// starting=false, success → all dots green (Theme.palette.success) +// starting=false, !success → red X pattern (Theme.palette.error) +Item { + id: root + + property bool starting: true + property bool success: false + property int animPhase: 0 + + readonly property int columns: 7 + readonly property int dotSize: 8 + readonly property int dotSpacing: 5 + + implicitWidth: columns * dotSize + (columns - 1) * dotSpacing + implicitHeight: columns * dotSize + (columns - 1) * dotSpacing + width: implicitWidth + height: implicitHeight + + Timer { + interval: 120 + repeat: true + running: root.starting + onTriggered: root.animPhase = (root.animPhase + 1) % 14 + } + + Grid { + columns: root.columns + spacing: root.dotSpacing + + Repeater { + model: root.columns * root.columns + + Rectangle { + width: root.dotSize + height: root.dotSize + radius: root.dotSize * 0.25 + + color: { + if (root.success) return Theme.palette.success + if (!root.starting) return Theme.palette.error + return Theme.palette.text + } + + opacity: { + const col = index % root.columns + const row = Math.floor(index / root.columns) + const d = Math.abs(col - 3) + Math.abs(row - 3) + + if (root.starting) { + const wave = root.animPhase % root.columns + const diff = Math.abs(d - wave) + if (diff === 0) return 0.9 + if (diff === 1) return 0.35 + return 0.1 + } + + if (root.success) return 0.85 + + // Error — X pattern + return (col === row || col + row === 6) ? 0.9 : 0.1 + } + } + } + } +} From 20494925198a398e360a29c413cd169ff8fadb1a Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 20 Feb 2026 15:40:23 +0400 Subject: [PATCH 12/13] Add icons --- src/qml/AdvancedIcon.qml | 12 ++++++++++++ src/qml/GuideIcon.qml | 12 ++++++++++++ src/qml/PortIcon.qml | 12 ++++++++++++ src/qml/UpnpIcon.qml | 12 ++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 src/qml/AdvancedIcon.qml create mode 100644 src/qml/GuideIcon.qml create mode 100644 src/qml/PortIcon.qml create mode 100644 src/qml/UpnpIcon.qml diff --git a/src/qml/AdvancedIcon.qml b/src/qml/AdvancedIcon.qml new file mode 100644 index 0000000..b81ea6d --- /dev/null +++ b/src/qml/AdvancedIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// X pattern — advanced / expert mode +DotIcon { + pattern: [ + 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, + 1, 0, 0, 0, 1 + ] +} diff --git a/src/qml/GuideIcon.qml b/src/qml/GuideIcon.qml new file mode 100644 index 0000000..b6be23a --- /dev/null +++ b/src/qml/GuideIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Crosshair pattern — step-by-step guide +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 1, 1, 0, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0 + ] +} diff --git a/src/qml/PortIcon.qml b/src/qml/PortIcon.qml new file mode 100644 index 0000000..0a3cafb --- /dev/null +++ b/src/qml/PortIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Right arrow pattern — manual port forwarding +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 0, + 0, 0, 1, 0, 0 + ] +} diff --git a/src/qml/UpnpIcon.qml b/src/qml/UpnpIcon.qml new file mode 100644 index 0000000..f59e08c --- /dev/null +++ b/src/qml/UpnpIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Diamond / network pattern — UPnP automatic port forwarding +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0 + ] +} From 9ac02311dfe81d2b81de07692b9f8f5b07bf63fd Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 20 Feb 2026 15:50:53 +0400 Subject: [PATCH 13/13] Add more consistency using the theme variables --- README.md | 35 +++++++++++++++++ src/qml/AdvancedSetup.qml | 37 ++++++----------- src/qml/CMakeLists.txt | 6 +++ src/qml/HealthIndicator.qml | 5 ++- src/qml/LogosStorageButton.qml | 23 ++++++++++- src/qml/ModeSelector.qml | 66 +++++++------------------------ src/qml/OnBoarding.qml | 72 +++++++--------------------------- src/qml/PortForwarding.qml | 44 +++++++++++++-------- src/qml/StartNode.qml | 56 ++++++++++++-------------- src/storage_resources.qrc | 8 ++++ 10 files changed, 167 insertions(+), 185 deletions(-) diff --git a/README.md b/README.md index b1ccea3..8fe864b 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,41 @@ To restart the onboarding process, simply delete the prefences file and relaunch The application also provides a JSON editor in the debug panel for runtime configuration tweaks. To apply changes, restart the Storage Module. +## Troubleshooting + +### Node has no peers + +**Symptom:** +The node starts successfully but never connects to any peer. + +**Cause:** +Logos Storage uses a discovery port (default `8090`) to announce itself to the DHT and find peers. If this port is already use by another process, the DHT cannot work properly. + +**Fix:** +Ensure that no process is running on `8090` process or change the default port value in the advanced configuration. + +### UPnP not working + +**Symptom:** +You selected UPnP during setup but the node remains unreachable. + +**Cause:** +UPnP relies on your router supporting and enabling the UPnP protocol. Many routers have it disabled by default for security reasons. + +**Fix:** +Make sure UPnP is enabled on your router or switch to port forwarding config. + +### Manual port forwarding + +**Symptom:** +You configure the port forwarding with a TCP port but the node remains unreachable. + +**Cause:** +The port is not open on your router. + +**Fix:** +Make port forwarding is enabled for this port on your router. + #### Nix Organization The nix build system is organized into modular files in the `/nix` directory: diff --git a/src/qml/AdvancedSetup.qml b/src/qml/AdvancedSetup.qml index e53bda8..8e23e22 100644 --- a/src/qml/AdvancedSetup.qml +++ b/src/qml/AdvancedSetup.qml @@ -36,9 +36,9 @@ LogosStorageLayout { Rectangle { Layout.fillWidth: true Layout.fillHeight: true - color: "#1e1e1e" + color: Theme.palette.backgroundElevated radius: 8 - border.color: jsonArea.isValid ? "#3a3a3a" : "#ff0000" + border.color: jsonArea.isValid ? Theme.palette.borderSecondary : Theme.palette.error border.width: 1 ScrollView { @@ -49,7 +49,7 @@ LogosStorageLayout { id: jsonArea font.family: "monospace" font.pixelSize: 12 - color: "#d4d4d4" + color: Theme.palette.text wrapMode: Text.WrapAnywhere background: Item {} @@ -85,29 +85,14 @@ LogosStorageLayout { onClicked: root.back() } - Rectangle { - width: 120 - height: 36 - radius: 8 - color: jsonArea.isValid ? "#4CAF50" : "#444444" - - Text { - anchors.centerIn: parent - text: "Validate" - color: "white" - font.pixelSize: 14 - font.bold: true - } - - MouseArea { - anchors.fill: parent - enabled: jsonArea.isValid - cursorShape: jsonArea.isValid ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: function () { - root.backend.saveUserConfig(jsonArea.text) - root.backend.reloadIfChanged(jsonArea.text) - root.completed() - } + LogosStorageButton { + text: "Validate" + variant: "success" + enabled: jsonArea.isValid + onClicked: { + root.backend.saveUserConfig(jsonArea.text) + root.backend.reloadIfChanged(jsonArea.text) + root.completed() } } } diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index 1e360ef..a96ce58 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -138,6 +138,12 @@ qt_add_qml_module(appqml HealthIndicator.qml ModeSelector.qml AdvancedSetup.qml + DotIcon.qml + NodeStatusIcon.qml + GuideIcon.qml + AdvancedIcon.qml + UpnpIcon.qml + PortIcon.qml ) # Set up QML module directory for runtime diff --git a/src/qml/HealthIndicator.qml b/src/qml/HealthIndicator.qml index 74a7aa2..3f18b7c 100644 --- a/src/qml/HealthIndicator.qml +++ b/src/qml/HealthIndicator.qml @@ -1,4 +1,5 @@ import QtQuick +import Logos.Theme Item { id: root @@ -57,14 +58,14 @@ Item { height: 10 radius: 5 anchors.verticalCenter: parent.verticalCenter - color: root.nodeIsUp ? "#4CAF50" : "#f44336" + color: root.nodeIsUp ? Theme.palette.success : Theme.palette.error opacity: root.blinkOn ? 1.0 : 0.15 } Text { anchors.verticalCenter: parent.verticalCenter text: root.nodeIsUp ? "Node reachable" : "Node unreachable" - color: root.nodeIsUp ? "#4CAF50" : "#f44336" + color: root.nodeIsUp ? Theme.palette.success : Theme.palette.error font.pixelSize: 12 } } diff --git a/src/qml/LogosStorageButton.qml b/src/qml/LogosStorageButton.qml index 8b5bfb1..90fcddf 100644 --- a/src/qml/LogosStorageButton.qml +++ b/src/qml/LogosStorageButton.qml @@ -6,10 +6,21 @@ Button { id: control padding: Theme.spacing.small + // "default" | "success" + property string variant: "default" + + readonly property bool isSuccess: variant === "success" + contentItem: Text { text: control.text font.pixelSize: Theme.typography.primaryText - color: control.enabled ? Theme.palette.text : Theme.palette.textMuted + color: { + if (!control.enabled) + return Theme.palette.textMuted + if (control.isSuccess) + return Theme.palette.background + return Theme.palette.text + } horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } @@ -18,10 +29,18 @@ Button { color: { if (!control.enabled) return Theme.palette.backgroundElevated + if (control.isSuccess) + return Theme.palette.success return Theme.palette.backgroundSecondary } border.width: 1 - border.color: Theme.palette.border + border.color: { + if (!control.enabled) + return Theme.palette.border + if (control.isSuccess) + return Theme.palette.success + return Theme.palette.border + } radius: Theme.spacing.tiny Behavior on color { diff --git a/src/qml/ModeSelector.qml b/src/qml/ModeSelector.qml index ffd14d7..eaac2c9 100644 --- a/src/qml/ModeSelector.qml +++ b/src/qml/ModeSelector.qml @@ -8,7 +8,7 @@ LogosStorageLayout { signal completed(bool isGuide) - property int selectedMode: -1 // 0 = guide, 1 = advanced + property int selectedMode: -1 ColumnLayout { anchors.centerIn: parent @@ -30,9 +30,7 @@ LogosStorageLayout { Layout.fillWidth: true } - Item { - height: Theme.spacing.medium - } + Item { height: Theme.spacing.medium } Row { spacing: Theme.spacing.medium @@ -43,38 +41,22 @@ LogosStorageLayout { width: 190 height: 230 radius: 14 - color: root.selectedMode === 0 ? Qt.rgba(1, 1, 1, - 0.08) : "transparent" - border.color: root.selectedMode === 0 ? "white" : Qt.rgba(1, 1, - 1, - 0.2) + color: root.selectedMode === 0 ? Theme.palette.overlayLight : "transparent" + border.color: root.selectedMode === 0 ? Theme.palette.text : Theme.palette.borderTertiaryMuted border.width: root.selectedMode === 0 ? 2 : 1 ColumnLayout { anchors.centerIn: parent spacing: 14 - // Nothing OS dot icon like - Grid { - columns: 5 - spacing: 4 + GuideIcon { + dotColor: Theme.palette.text Layout.alignment: Qt.AlignHCenter - - Repeater { - model: [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0] - Rectangle { - width: 6 - height: 6 - radius: 2 - color: "white" - opacity: modelData ? 0.9 : 0.1 - } - } } Text { text: "Guide" - color: "white" + color: Theme.palette.text font.pixelSize: 16 font.bold: true Layout.alignment: Qt.AlignHCenter @@ -82,7 +64,7 @@ LogosStorageLayout { Text { text: "Step-by-step setup.\nRecommended for\nmost users." - color: Qt.rgba(1, 1, 1, 0.55) + color: Theme.palette.textSecondary font.pixelSize: 12 horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter @@ -103,38 +85,22 @@ LogosStorageLayout { width: 190 height: 230 radius: 14 - color: root.selectedMode === 1 ? Qt.rgba(1, 1, 1, - 0.08) : "transparent" - border.color: root.selectedMode === 1 ? "white" : Qt.rgba(1, 1, - 1, - 0.2) + color: root.selectedMode === 1 ? Theme.palette.overlayLight : "transparent" + border.color: root.selectedMode === 1 ? Theme.palette.text : Theme.palette.borderTertiaryMuted border.width: root.selectedMode === 1 ? 2 : 1 ColumnLayout { anchors.centerIn: parent spacing: 14 - // Nothing OS dot icon like - Grid { - columns: 5 - spacing: 4 + AdvancedIcon { + dotColor: Theme.palette.text Layout.alignment: Qt.AlignHCenter - - Repeater { - model: [1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1] - Rectangle { - width: 6 - height: 6 - radius: 2 - color: "white" - opacity: modelData ? 0.9 : 0.1 - } - } } Text { text: "Advanced" - color: "white" + color: Theme.palette.text font.pixelSize: 16 font.bold: true Layout.alignment: Qt.AlignHCenter @@ -142,7 +108,7 @@ LogosStorageLayout { Text { text: "Manual JSON\nconfiguration for\nexperienced users." - color: Qt.rgba(1, 1, 1, 0.55) + color: Theme.palette.textSecondary font.pixelSize: 12 horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter @@ -159,9 +125,7 @@ LogosStorageLayout { } } - Item { - height: Theme.spacing.small - } + Item { height: Theme.spacing.small } LogosStorageButton { text: "Continue" diff --git a/src/qml/OnBoarding.qml b/src/qml/OnBoarding.qml index e3d8488..4843c89 100644 --- a/src/qml/OnBoarding.qml +++ b/src/qml/OnBoarding.qml @@ -11,7 +11,7 @@ LogosStorageLayout { signal back signal completed(bool upnpEnabled) - property int selectedMode: -1 // 0 = upnp, 1 = port forwarding + property int selectedMode: -1 ColumnLayout { anchors.centerIn: parent @@ -33,9 +33,7 @@ LogosStorageLayout { Layout.fillWidth: true } - Item { - height: Theme.spacing.medium - } + Item { height: Theme.spacing.medium } Row { spacing: Theme.spacing.medium @@ -46,41 +44,22 @@ LogosStorageLayout { width: 190 height: 230 radius: 14 - color: root.selectedMode === 0 ? Qt.rgba(1, 1, 1, 0.08) : "transparent" - border.color: root.selectedMode === 0 ? "white" : Qt.rgba(1, 1, 1, 0.2) + color: root.selectedMode === 0 ? Theme.palette.overlayLight : "transparent" + border.color: root.selectedMode === 0 ? Theme.palette.text : Theme.palette.borderTertiaryMuted border.width: root.selectedMode === 0 ? 2 : 1 ColumnLayout { anchors.centerIn: parent spacing: 14 - // Nothing OS dot icon — diamond/network - Grid { - columns: 5 - spacing: 4 + UpnpIcon { + dotColor: Theme.palette.text Layout.alignment: Qt.AlignHCenter - - Repeater { - model: [ - 0, 0, 1, 0, 0, - 0, 1, 0, 1, 0, - 1, 0, 1, 0, 1, - 0, 1, 0, 1, 0, - 0, 0, 1, 0, 0 - ] - Rectangle { - width: 6 - height: 6 - radius: 2 - color: "white" - opacity: modelData ? 0.9 : 0.1 - } - } } Text { text: "UPnP" - color: "white" + color: Theme.palette.text font.pixelSize: 16 font.bold: true Layout.alignment: Qt.AlignHCenter @@ -88,7 +67,7 @@ LogosStorageLayout { Text { text: "Automatic port\nforwarding via\nUPnP router." - color: Qt.rgba(1, 1, 1, 0.55) + color: Theme.palette.textSecondary font.pixelSize: 12 horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter @@ -109,41 +88,22 @@ LogosStorageLayout { width: 190 height: 230 radius: 14 - color: root.selectedMode === 1 ? Qt.rgba(1, 1, 1, 0.08) : "transparent" - border.color: root.selectedMode === 1 ? "white" : Qt.rgba(1, 1, 1, 0.2) + color: root.selectedMode === 1 ? Theme.palette.overlayLight : "transparent" + border.color: root.selectedMode === 1 ? Theme.palette.text : Theme.palette.borderTertiaryMuted border.width: root.selectedMode === 1 ? 2 : 1 ColumnLayout { anchors.centerIn: parent spacing: 14 - // Nothing OS dot icon — right arrow - Grid { - columns: 5 - spacing: 4 + PortIcon { + dotColor: Theme.palette.text Layout.alignment: Qt.AlignHCenter - - Repeater { - model: [ - 0, 0, 1, 0, 0, - 0, 0, 0, 1, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 0, - 0, 0, 1, 0, 0 - ] - Rectangle { - width: 6 - height: 6 - radius: 2 - color: "white" - opacity: modelData ? 0.9 : 0.1 - } - } } Text { text: "Port Forwarding" - color: "white" + color: Theme.palette.text font.pixelSize: 16 font.bold: true Layout.alignment: Qt.AlignHCenter @@ -151,7 +111,7 @@ LogosStorageLayout { Text { text: "Manual TCP port\nconfiguration on\nyour router." - color: Qt.rgba(1, 1, 1, 0.55) + color: Theme.palette.textSecondary font.pixelSize: 12 horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter @@ -168,9 +128,7 @@ LogosStorageLayout { } } - Item { - height: Theme.spacing.small - } + Item { height: Theme.spacing.small } RowLayout { Layout.alignment: Qt.AlignHCenter diff --git a/src/qml/PortForwarding.qml b/src/qml/PortForwarding.qml index 7698476..26bc48b 100644 --- a/src/qml/PortForwarding.qml +++ b/src/qml/PortForwarding.qml @@ -16,10 +16,6 @@ LogosStorageLayout { Connections { target: root.backend - // The nat ext checking needs a bit of - // time because the Storage backend retrieves - // the public IP by making a call to the echo service. - // When the config is done, just push the startNodeComponent. function onNatExtConfigCompleted() { root.loading = false root.completed(root.tcpPort) @@ -30,20 +26,26 @@ LogosStorageLayout { anchors.centerIn: parent spacing: Theme.spacing.medium width: 400 - Layout.fillWidth: true - LogosText { - id: questionText - font.pixelSize: Theme.typography.titleText - text: "Choose your TCP port" - Layout.alignment: Qt.AlignCenter + PortIcon { + animated: root.loading + dotColor: Theme.palette.text + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + font.pixelSize: Theme.typography.titleText + text: "Port Configuration" + Layout.alignment: Qt.AlignHCenter } LogosText { - id: questionDescriptionText font.pixelSize: Theme.typography.primaryText - text: "The TCP port has to be open to connect with other remote peers." - Layout.alignment: Qt.AlignCenter + text: "The TCP port must be open to connect with remote peers." + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.fillWidth: true } LogosTextField { @@ -64,14 +66,14 @@ LogosStorageLayout { } } - Row { + RowLayout { + Layout.alignment: Qt.AlignHCenter spacing: Theme.spacing.small LogosStorageButton { text: "Back" - onClicked: { - root.back() - } + enabled: !root.loading + onClicked: root.back() } LogosStorageButton { @@ -83,6 +85,14 @@ LogosStorageLayout { } } } + + LogosText { + font.pixelSize: Theme.typography.primaryText + text: "Retrieving your public IP..." + color: Theme.palette.textTertiary + visible: root.loading + Layout.alignment: Qt.AlignHCenter + } } QtObject { diff --git a/src/qml/StartNode.qml b/src/qml/StartNode.qml index a52480b..7c466fb 100644 --- a/src/qml/StartNode.qml +++ b/src/qml/StartNode.qml @@ -9,7 +9,7 @@ LogosStorageLayout { property var backend: mockBackend property string status: "" - property string title: "Starting your node...." + property string title: "Starting your node" property string resolution: "" property bool starting: true property bool success: false @@ -19,15 +19,13 @@ LogosStorageLayout { function onNodeStarted() { root.starting = false - root.status = "Logos Storage started successfully." - root.title = "Success" + root.status = "Your node is up and reachable." + root.title = "Node is ready" root.success = true } Component.onCompleted: root.backend.start() - // Wait after startCompleted before calling checkNodeIs to - // make sure the the node is started and ready. Timer { id: nodeCheckTimer interval: 500 @@ -39,9 +37,8 @@ LogosStorageLayout { target: root.backend function onStartCompleted() { - console.info("startCompleted") - root.title = "Checking.." - root.status = "Your node is started, checking everything is up." + root.title = "Checking connectivity" + root.status = "Node started, verifying reachability..." nodeCheckTimer.start() } @@ -57,7 +54,7 @@ LogosStorageLayout { function onNodeIsntUp(reason) { root.starting = false - root.title = "Node not reachable" + root.title = "Node unreachable" root.status = "" root.resolution = reason } @@ -69,28 +66,35 @@ LogosStorageLayout { spacing: Theme.spacing.medium LogosText { - id: titleText font.pixelSize: Theme.typography.titleText text: root.title Layout.alignment: Qt.AlignHCenter } - LogosText { - id: statusText - font.pixelSize: Theme.typography.primaryText - text: root.status + NodeStatusIcon { + starting: root.starting + success: root.success Layout.alignment: Qt.AlignHCenter - wrapMode: Text.WordWrap } LogosText { - id: resolutionText + font.pixelSize: Theme.typography.primaryText + text: root.status + visible: root.status !== "" + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + LogosText { font.pixelSize: Theme.typography.primaryText text: root.resolution visible: root.resolution !== "" color: Theme.palette.error wrapMode: Text.WordWrap Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter Layout.alignment: Qt.AlignHCenter } } @@ -101,12 +105,11 @@ LogosStorageLayout { anchors.bottomMargin: 10 anchors.leftMargin: 10 text: "Back" - onClicked: function () { + enabled: !root.starting + onClicked: { root.backend.stop() root.back() } - - enabled: root.starting == false } LogosStorageButton { @@ -115,40 +118,33 @@ LogosStorageLayout { anchors.bottomMargin: 10 anchors.rightMargin: 10 text: "Next" - onClicked: function () { + enabled: root.success + onClicked: { root.backend.saveCurrentConfig() root.next() } - enabled: root.success == true } - // In preview/mock mode, simulate a successful node start after 2 seconds Timer { interval: 2000 running: root.backend && root.backend.isMock === true - onTriggered: root.onNodeStarted() repeat: false + onTriggered: root.onNodeStarted() } QtObject { id: mockBackend readonly property bool isMock: true - property string configJson: "{}" signal startCompleted signal startFailed(string error) signal nodeIsUp signal nodeIsntUp(string reason) - function guessResolution() { - return "" - } - function checkNodeIsUp() {} - function stop() {} - function saveCurrentConfig() {} + function start() {} } } diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index fe9cd5e..646780a 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -10,5 +10,13 @@ qml/PortForwarding.qml qml/ErrorToast.qml qml/HealthIndicator.qml + qml/ModeSelector.qml + qml/AdvancedSetup.qml + qml/DotIcon.qml + qml/NodeStatusIcon.qml + qml/GuideIcon.qml + qml/AdvancedIcon.qml + qml/UpnpIcon.qml + qml/PortIcon.qml