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);