From a7d1e092a3355e5918fef1996eb044534de1cb39 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 20 Feb 2026 15:07:58 +0400 Subject: [PATCH] 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