mirror of
https://github.com/logos-storage/logos-storage-app-skeleton.git
synced 2026-06-15 12:59:29 +00:00
Merge pull request #11 from logos-co/feat/improve-onboarding
feat: improve onboarding
This commit is contained in:
commit
30dd14c966
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
|
||||
35
README.md
35
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:
|
||||
|
||||
1
logos-design-system
Submodule
1
logos-design-system
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 596811cbb0a0644322267368e87fab80e34203d8
|
||||
@ -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
|
||||
|
||||
@ -8,6 +8,10 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLocale>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QNetworkReply>
|
||||
#include <QSslSocket>
|
||||
|
||||
// StorageBackend is responsible for managing the interaction with the storage module.
|
||||
// It is mocked in the QML.
|
||||
@ -20,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;
|
||||
@ -28,6 +35,8 @@ StorageBackend::StorageBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
}
|
||||
|
||||
m_logos = new LogosModules(m_logosAPI);
|
||||
|
||||
emit ready();
|
||||
}
|
||||
|
||||
StorageBackend::~StorageBackend()
|
||||
@ -36,21 +45,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;
|
||||
reportError("Failed to create the storage: invalid JSON config");
|
||||
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");
|
||||
return {false, "", "Filed to init storage"};
|
||||
reportError("Failed to init storage");
|
||||
return {false, "", "Failed to init storage"};
|
||||
}
|
||||
|
||||
setStatus(Stopped);
|
||||
@ -63,11 +75,12 @@ 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.");
|
||||
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();
|
||||
}
|
||||
})) {
|
||||
@ -195,13 +208,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, ""};
|
||||
}
|
||||
@ -233,6 +241,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) {
|
||||
@ -291,6 +301,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";
|
||||
@ -723,12 +738,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");
|
||||
@ -762,30 +772,24 @@ StorageBackend::StorageStatus StorageBackend::status() const { return m_status;
|
||||
|
||||
QString StorageBackend::cid() const { return m_cid; }
|
||||
|
||||
QString StorageBackend::configJson() const { return m_configJson; }
|
||||
|
||||
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...");
|
||||
@ -803,105 +807,216 @@ 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::Indented)));
|
||||
}
|
||||
|
||||
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::buildConfigFromFile(const QString& path) {
|
||||
qDebug() << "StorageBackend::buildConfigFromFile called";
|
||||
void StorageBackend::enableUpnpConfig() {
|
||||
debug("StorageBackend::enableUpnpConfig called");
|
||||
|
||||
QFile file(path);
|
||||
if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
QString configJson = QString::fromUtf8(file.readAll());
|
||||
QJsonDocument doc = defaultConfig();
|
||||
QJsonObject obj = doc.object();
|
||||
|
||||
debug("StorageUIPlugin: config.json is found, configJson=" + configJson);
|
||||
obj["nat"] = "upnp";
|
||||
|
||||
return configJson;
|
||||
reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Indented)));
|
||||
}
|
||||
|
||||
void StorageBackend::enableNatExtConfig(int tcpPort) {
|
||||
qDebug() << "StorageBackend::enableNatExtConfig called with tcpPort" << tcpPort;
|
||||
|
||||
QJsonDocument doc = defaultConfig();
|
||||
QJsonObject obj = doc.object();
|
||||
|
||||
QJsonArray listenAddrs = {QString("/ip4/0.0.0.0/tcp/%1").arg(tcpPort)};
|
||||
obj["listen-addrs"] = listenAddrs;
|
||||
|
||||
// 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...");
|
||||
|
||||
QNetworkAccessManager* manager = new QNetworkAccessManager(this);
|
||||
QNetworkRequest request(ECHO_PROVIDER);
|
||||
|
||||
// Set text/plain to receive only the IP
|
||||
request.setRawHeader("Accept", "text/plain");
|
||||
|
||||
QNetworkReply* reply = manager->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, manager, obj]() mutable {
|
||||
reply->deleteLater();
|
||||
manager->deleteLater();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)));
|
||||
emit natExtConfigCompleted();
|
||||
});
|
||||
}
|
||||
|
||||
void StorageBackend::checkNodeIsUp() {
|
||||
qDebug() << "StorageBackend::checkNodeIsUp called.";
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
debug("StorageUIPlugin: Failed to load config.json");
|
||||
return "{}";
|
||||
// Ensure that the node has at least one peer.
|
||||
QVariantMap table = result.getValue<QVariantMap>("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<QVariantList>("announceAddresses");
|
||||
QList<int> 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; }
|
||||
|
||||
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::Indented)));
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "logos_api.h"
|
||||
#include "logos_sdk.h"
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
@ -10,6 +11,10 @@
|
||||
|
||||
static const int RET_OK = 0;
|
||||
static const int RET_PROGRESS = 3;
|
||||
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";
|
||||
|
||||
// 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,13 +43,25 @@ 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;
|
||||
QString debugLogs() const;
|
||||
StorageStatus status() const;
|
||||
QString configJson() const;
|
||||
int uploadProgress() const;
|
||||
QString uploadStatus() const;
|
||||
QVariantList manifests() const;
|
||||
@ -53,9 +69,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,25 +96,77 @@ 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 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);
|
||||
|
||||
// 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:
|
||||
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 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:
|
||||
|
||||
@ -108,13 +174,13 @@ 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;
|
||||
StorageStatus m_status;
|
||||
QString m_debugLogs;
|
||||
QString m_cid;
|
||||
QString m_configJson;
|
||||
int m_uploadProgress = 0;
|
||||
QString m_uploadStatus = "";
|
||||
qint64 m_uploadTotalBytes = 0;
|
||||
@ -123,4 +189,5 @@ class StorageBackend : public QObject {
|
||||
qint64 m_quotaMaxBytes = 0;
|
||||
qint64 m_quotaUsedBytes = 0;
|
||||
qint64 m_quotaReservedBytes = 0;
|
||||
QJsonDocument m_config;
|
||||
};
|
||||
|
||||
@ -54,29 +54,19 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
|
||||
root->setProperty("backend", QVariant::fromValue(static_cast<QObject*>(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;
|
||||
}
|
||||
|
||||
@ -112,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();
|
||||
|
||||
12
src/qml/AdvancedIcon.qml
Normal file
12
src/qml/AdvancedIcon.qml
Normal file
@ -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
|
||||
]
|
||||
}
|
||||
100
src/qml/AdvancedSetup.qml
Normal file
100
src/qml/AdvancedSetup.qml
Normal file
@ -0,0 +1,100 @@
|
||||
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
|
||||
|
||||
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: Theme.palette.backgroundElevated
|
||||
radius: 8
|
||||
border.color: jsonArea.isValid ? Theme.palette.borderSecondary : Theme.palette.error
|
||||
border.width: 1
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
|
||||
TextArea {
|
||||
id: jsonArea
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
color: Theme.palette.text
|
||||
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()
|
||||
}
|
||||
|
||||
LogosStorageButton {
|
||||
text: "Validate"
|
||||
variant: "success"
|
||||
enabled: jsonArea.isValid
|
||||
onClicked: {
|
||||
root.backend.saveUserConfig(jsonArea.text)
|
||||
root.backend.reloadIfChanged(jsonArea.text)
|
||||
root.completed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,6 +132,18 @@ qt_add_qml_module(appqml
|
||||
StartNode.qml
|
||||
LogosTextField.qml
|
||||
LogosStorageButton.qml
|
||||
LogosStorageLayout.qml
|
||||
PortForwarding.qml
|
||||
ErrorToast.qml
|
||||
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
|
||||
|
||||
74
src/qml/DotIcon.qml
Normal file
74
src/qml/DotIcon.qml
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/qml/ErrorToast.qml
Normal file
125
src/qml/ErrorToast.qml
Normal file
@ -0,0 +1,125 @@
|
||||
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(title, message) {
|
||||
root.title = title
|
||||
root.message = message
|
||||
root.visible = true
|
||||
slideAnim.restart()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
hideAnim.restart()
|
||||
}
|
||||
|
||||
visible: false
|
||||
opacity: 0
|
||||
width: 500
|
||||
radius: Theme.spacing.tiny
|
||||
color: "#3D2020"
|
||||
|
||||
implicitHeight: content.implicitHeight + Theme.spacing.medium * 2
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
margins: Theme.spacing.medium
|
||||
}
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
LogosText {
|
||||
id: titleText
|
||||
Layout.fillWidth: true
|
||||
color: Theme.palette.error
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
LogosText {
|
||||
id: messageText
|
||||
Layout.fillWidth: true
|
||||
color: Theme.palette.text
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/qml/GuideIcon.qml
Normal file
12
src/qml/GuideIcon.qml
Normal file
@ -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
|
||||
]
|
||||
}
|
||||
79
src/qml/HealthIndicator.qml
Normal file
79
src/qml/HealthIndicator.qml
Normal file
@ -0,0 +1,79 @@
|
||||
import QtQuick
|
||||
import Logos.Theme
|
||||
|
||||
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 ? 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 ? Theme.palette.success : Theme.palette.error
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
signal nodeIsUp
|
||||
signal nodeIsntUp(string reason)
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
12
src/qml/LogosStorageLayout.qml
Normal file
12
src/qml/LogosStorageLayout.qml
Normal file
@ -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
|
||||
}
|
||||
231
src/qml/Main.qml
231
src/qml/Main.qml
@ -2,139 +2,212 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtCore
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// 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: 600
|
||||
implicitHeight: 400
|
||||
implicitWidth: 800
|
||||
implicitHeight: 800
|
||||
|
||||
property var backend: mockBackend
|
||||
|
||||
// Timer {
|
||||
// readonly property int running: 2
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
// 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
|
||||
// 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) {
|
||||
|
||||
property int status
|
||||
// // stackView.pop()
|
||||
// }
|
||||
// }
|
||||
|
||||
signal startCompleted
|
||||
signal startFailed
|
||||
signal stopCompleted
|
||||
|
||||
function updateBasicConfig(dataDir, discPort) {
|
||||
console.log("updateBasicConfig", dataDir, discPort)
|
||||
// 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()
|
||||
stackView.replace(storageComponent, StackView.Immediate)
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
// timer.start()
|
||||
console.log("mock start callde")
|
||||
// If there is any error, display it in a toast view
|
||||
function onError(message) {
|
||||
errorToast.show("Error", message)
|
||||
}
|
||||
|
||||
function stop() {
|
||||
root.backend.stopCompleted()
|
||||
}
|
||||
|
||||
function defaultDataDir() {
|
||||
return ".cache/storage"
|
||||
}
|
||||
|
||||
function buildConfig() {}
|
||||
|
||||
function reloadIfChanged() {}
|
||||
|
||||
function init() {}
|
||||
}
|
||||
|
||||
Settings {
|
||||
id: settings
|
||||
category: "Storage"
|
||||
|
||||
property int discoveryPort: 8090
|
||||
property int tcpPort: 0
|
||||
property string dataDir: ""
|
||||
property bool onboardingCompleted: false
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
anchors.fill: parent
|
||||
initialItem: onboarding
|
||||
initialItem: modeSelectorComponent
|
||||
}
|
||||
|
||||
Component {
|
||||
id: onboarding
|
||||
id: modeSelectorComponent
|
||||
|
||||
OnBoarding {
|
||||
id: onboardingInstance
|
||||
backend: root.backend
|
||||
discoveryPort: settings.discoveryPort
|
||||
tcpPort: settings.tcpPort
|
||||
dataDir: settings.dataDir.length > 0 ? settings.dataDir : root.backend.defaultDataDir()
|
||||
|
||||
onCompleted: {
|
||||
settings.discoveryPort = discoveryPort
|
||||
settings.dataDir = dataDir
|
||||
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)
|
||||
ModeSelector {
|
||||
onCompleted: function (isGuide) {
|
||||
if (isGuide) {
|
||||
stackView.push(onboardingComponent)
|
||||
} else {
|
||||
stackView.push(advancedSetupComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: storageView
|
||||
id: onboardingComponent
|
||||
|
||||
OnBoarding {
|
||||
backend: root.backend
|
||||
|
||||
onBack: stackView.pop()
|
||||
|
||||
onCompleted: function (upnpEnabled) {
|
||||
if (upnpEnabled) {
|
||||
stackView.push(startNodeComponent)
|
||||
} else {
|
||||
stackView.push(portForwardingComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: advancedSetupComponent
|
||||
|
||||
AdvancedSetup {
|
||||
backend: root.backend
|
||||
|
||||
onBack: stackView.pop()
|
||||
|
||||
onCompleted: function () {
|
||||
settings.onboardingCompleted = true
|
||||
stackView.replace(storageComponent, StackView.Immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
settings.onboardingCompleted = true
|
||||
stackView.push(storageComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
Component {
|
||||
id: portForwardingComponent
|
||||
|
||||
function onStopCompleted() {
|
||||
stackView.pop()
|
||||
PortForwarding {
|
||||
backend: root.backend
|
||||
loading: false
|
||||
|
||||
onBack: {
|
||||
stackView.pop()
|
||||
}
|
||||
|
||||
onCompleted: function () {
|
||||
stackView.push(startNodeComponent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ErrorToast {
|
||||
id: errorToast
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Theme.spacing.medium
|
||||
}
|
||||
|
||||
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() {
|
||||
if (settings.onboardingCompleted) {
|
||||
root.backend.start()
|
||||
stackView.replace(storageView, StackView.Immediate)
|
||||
}
|
||||
function saveUserConfig() {}
|
||||
|
||||
function loadUserConfig() {}
|
||||
|
||||
function reloadIfChanged() {}
|
||||
|
||||
function enableUpnpConfig() {}
|
||||
|
||||
function enableNatExtConfig() {
|
||||
natExtConfigCompleted()
|
||||
}
|
||||
|
||||
function saveCurrentConfig() {}
|
||||
|
||||
function stop() {}
|
||||
|
||||
function checkNodeIsUp() {}
|
||||
|
||||
function guessResolution() {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
137
src/qml/ModeSelector.qml
Normal file
137
src/qml/ModeSelector.qml
Normal file
@ -0,0 +1,137 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
LogosStorageLayout {
|
||||
id: root
|
||||
|
||||
signal completed(bool isGuide)
|
||||
|
||||
property int selectedMode: -1
|
||||
|
||||
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 ? 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
|
||||
|
||||
GuideIcon {
|
||||
dotColor: Theme.palette.text
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Guide"
|
||||
color: Theme.palette.text
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Step-by-step setup.\nRecommended for\nmost users."
|
||||
color: Theme.palette.textSecondary
|
||||
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 ? 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
|
||||
|
||||
AdvancedIcon {
|
||||
dotColor: Theme.palette.text
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Advanced"
|
||||
color: Theme.palette.text
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Manual JSON\nconfiguration for\nexperienced users."
|
||||
color: Theme.palette.textSecondary
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/qml/NodeStatusIcon.qml
Normal file
71
src/qml/NodeStatusIcon.qml
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,145 +1,160 @@
|
||||
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 string dataDir: backend.defaultDataDir()
|
||||
signal completed
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
signal back
|
||||
signal completed(bool upnpEnabled)
|
||||
|
||||
function defaultDataDir() {
|
||||
return ".cache/storage"
|
||||
}
|
||||
}
|
||||
property int selectedMode: -1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacing.medium
|
||||
width: 400
|
||||
width: 430
|
||||
|
||||
LogosText {
|
||||
id: titleText
|
||||
text: "Network Configuration"
|
||||
font.pixelSize: Theme.typography.titleText
|
||||
text: "Logos Storage"
|
||||
// anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: discoveryPortColumn
|
||||
spacing: Theme.spacing.tiny
|
||||
LogosText {
|
||||
text: "How is your network configured?"
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
LogosText {
|
||||
text: "Discovery port"
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.text
|
||||
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 ? 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
|
||||
|
||||
UpnpIcon {
|
||||
dotColor: Theme.palette.text
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "UPnP"
|
||||
color: Theme.palette.text
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Automatic port\nforwarding via\nUPnP router."
|
||||
color: Theme.palette.textSecondary
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
// ── Port Forwarding card ─────────────────────────────────────
|
||||
Rectangle {
|
||||
width: 190
|
||||
height: 230
|
||||
radius: 14
|
||||
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
|
||||
|
||||
PortIcon {
|
||||
dotColor: Theme.palette.text
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Port Forwarding"
|
||||
color: Theme.palette.text
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Manual TCP port\nconfiguration on\nyour router."
|
||||
color: Theme.palette.textSecondary
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: tcpPortColumn
|
||||
spacing: Theme.spacing.tiny
|
||||
Layout.fillWidth: true
|
||||
Item { height: Theme.spacing.small }
|
||||
|
||||
LogosText {
|
||||
text: "TCP port"
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.text
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
LogosStorageButton {
|
||||
text: "Back"
|
||||
onClicked: root.back()
|
||||
}
|
||||
|
||||
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)
|
||||
LogosStorageButton {
|
||||
text: "Continue"
|
||||
enabled: root.selectedMode !== -1
|
||||
onClicked: {
|
||||
if (root.selectedMode === 0) {
|
||||
root.backend.enableUpnpConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Theme.spacing.tiny
|
||||
Layout.fillWidth: true
|
||||
|
||||
LogosText {
|
||||
text: "Data dir"
|
||||
}
|
||||
|
||||
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
|
||||
root.completed(root.selectedMode === 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogosStorageButton {
|
||||
text: "Next"
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.bottomMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
enabled: discoveryPortTextField.acceptableInput
|
||||
&& tcpPortTextField.acceptableInput && dataDirTextField.isValid
|
||||
onClicked: root.completed()
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
function enableUpnpConfig() {}
|
||||
}
|
||||
}
|
||||
|
||||
103
src/qml/PortForwarding.qml
Normal file
103
src/qml/PortForwarding.qml
Normal file
@ -0,0 +1,103 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
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
|
||||
|
||||
function onNatExtConfigCompleted() {
|
||||
root.loading = false
|
||||
root.completed(root.tcpPort)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacing.medium
|
||||
width: 400
|
||||
|
||||
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 {
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
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 {
|
||||
Layout.fillWidth: true
|
||||
id: tcpPortTextField
|
||||
placeholderText: "Enter the TCP port"
|
||||
text: root.tcpPort
|
||||
enabled: !root.loading
|
||||
isValid: {
|
||||
const val = parseInt(text)
|
||||
return !isNaN(val) && val >= 0 && val <= 65535
|
||||
}
|
||||
onTextChanged: {
|
||||
const val = parseInt(text)
|
||||
if (!isNaN(val) && val >= 0 && val <= 65535) {
|
||||
root.tcpPort = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosStorageButton {
|
||||
text: "Back"
|
||||
enabled: !root.loading
|
||||
onClicked: root.back()
|
||||
}
|
||||
|
||||
LogosStorageButton {
|
||||
text: "Next"
|
||||
enabled: !root.loading && tcpPortTextField.isValid
|
||||
onClicked: {
|
||||
root.loading = true
|
||||
root.backend.enableNatExtConfig(root.tcpPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogosText {
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
text: "Retrieving your public IP..."
|
||||
color: Theme.palette.textTertiary
|
||||
visible: root.loading
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
function enableNatExtConfig(port) {}
|
||||
}
|
||||
}
|
||||
12
src/qml/PortIcon.qml
Normal file
12
src/qml/PortIcon.qml
Normal file
@ -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
|
||||
]
|
||||
}
|
||||
@ -1,57 +1,100 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
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
|
||||
property var backend: mockBackend
|
||||
property string status: ""
|
||||
property string title: "Starting your node"
|
||||
property string resolution: ""
|
||||
property bool starting: true
|
||||
property bool success: false
|
||||
|
||||
signal back
|
||||
signal next
|
||||
|
||||
function onNodeStarted() {
|
||||
root.starting = false
|
||||
root.status = "Your node is up and reachable."
|
||||
root.title = "Node is ready"
|
||||
root.success = true
|
||||
}
|
||||
|
||||
Component.onCompleted: root.backend.start()
|
||||
|
||||
Timer {
|
||||
id: nodeCheckTimer
|
||||
interval: 500
|
||||
repeat: false
|
||||
onTriggered: root.backend.checkNodeIsUp()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.backend
|
||||
|
||||
function onStartCompleted() {
|
||||
console.log("onStartCompleted received")
|
||||
root.starting = false
|
||||
root.status = "Logos Storage started successfully."
|
||||
root.success = true
|
||||
root.title = "Checking connectivity"
|
||||
root.status = "Node started, verifying reachability..."
|
||||
nodeCheckTimer.start()
|
||||
}
|
||||
|
||||
function onStartFailed(error) {
|
||||
console.log("onStartFailed received")
|
||||
root.starting = false
|
||||
root.status = "Failed to start: " + error
|
||||
root.title = "Failed to start"
|
||||
root.status = "Your node failed to start: " + error
|
||||
}
|
||||
|
||||
function onNodeIsUp() {
|
||||
root.onNodeStarted()
|
||||
}
|
||||
|
||||
function onNodeIsntUp(reason) {
|
||||
root.starting = false
|
||||
root.title = "Node unreachable"
|
||||
root.status = ""
|
||||
root.resolution = reason
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacing.medium
|
||||
width: 400
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
LogosText {
|
||||
id: titleText
|
||||
font.pixelSize: Theme.typography.titleText
|
||||
text: "Starting your node...."
|
||||
text: root.title
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NodeStatusIcon {
|
||||
starting: root.starting
|
||||
success: root.success
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
LogosText {
|
||||
id: statusText
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -62,8 +105,11 @@ Rectangle {
|
||||
anchors.bottomMargin: 10
|
||||
anchors.leftMargin: 10
|
||||
text: "Back"
|
||||
onClicked: root.back()
|
||||
enabled: root.starting == false
|
||||
enabled: !root.starting
|
||||
onClicked: {
|
||||
root.backend.stop()
|
||||
root.back()
|
||||
}
|
||||
}
|
||||
|
||||
LogosStorageButton {
|
||||
@ -72,7 +118,33 @@ Rectangle {
|
||||
anchors.bottomMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
text: "Next"
|
||||
onClicked: root.next()
|
||||
enabled: root.success == true
|
||||
enabled: root.success
|
||||
onClicked: {
|
||||
root.backend.saveCurrentConfig()
|
||||
root.next()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 2000
|
||||
running: root.backend && root.backend.isMock === true
|
||||
repeat: false
|
||||
onTriggered: root.onNodeStarted()
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
readonly property bool isMock: true
|
||||
|
||||
signal startCompleted
|
||||
signal startFailed(string error)
|
||||
signal nodeIsUp
|
||||
signal nodeIsntUp(string reason)
|
||||
|
||||
function checkNodeIsUp() {}
|
||||
function stop() {}
|
||||
function saveCurrentConfig() {}
|
||||
function start() {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,13 +4,9 @@ import QtQuick.Dialogs
|
||||
import QtQuick.Layouts
|
||||
import QtCore
|
||||
|
||||
Rectangle {
|
||||
// qmllint disable unqualified
|
||||
LogosStorageLayout {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: 600
|
||||
implicitHeight: 600
|
||||
color: "#000000"
|
||||
|
||||
property var backend: mockBackend
|
||||
readonly property int stopped: 0
|
||||
@ -57,15 +53,20 @@ Rectangle {
|
||||
return backend.status == running
|
||||
}
|
||||
|
||||
Component.onCompleted: root.backend.start()
|
||||
|
||||
QtObject {
|
||||
id: mockBackend
|
||||
|
||||
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 +75,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) {
|
||||
@ -145,6 +89,11 @@ Rectangle {
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
|
||||
}
|
||||
|
||||
HealthIndicator {
|
||||
backend: root.backend
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Text {
|
||||
id: statusTextElement
|
||||
objectName: "status"
|
||||
@ -157,13 +106,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 +167,7 @@ Rectangle {
|
||||
implicitHeight: 6
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * parent.parent.visualPosition
|
||||
width: parent.width * control.visualPosition
|
||||
height: parent.height
|
||||
radius: 3
|
||||
color: "#4CAF50"
|
||||
|
||||
12
src/qml/UpnpIcon.qml
Normal file
12
src/qml/UpnpIcon.qml
Normal file
@ -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
|
||||
]
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QtQml>
|
||||
#include <QQmlDebuggingEnabler>
|
||||
|
||||
static QQmlTriviallyDestructibleDebuggingEnabler enabler;
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QGuiApplication app(argc, argv);
|
||||
@ -16,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();
|
||||
}
|
||||
|
||||
@ -6,6 +6,18 @@
|
||||
<file alias="StorageView.qml">qml/StorageView.qml</file>
|
||||
<file alias="LogosTextField.qml">qml/LogosTextField.qml</file>
|
||||
<file alias="LogosStorageButton.qml">qml/LogosStorageButton.qml</file>
|
||||
<file alias="LogosStorageLayout.qml">qml/LogosStorageLayout.qml</file>
|
||||
<file alias="PortForwarding.qml">qml/PortForwarding.qml</file>
|
||||
<file alias="ErrorToast.qml">qml/ErrorToast.qml</file>
|
||||
<file alias="HealthIndicator.qml">qml/HealthIndicator.qml</file>
|
||||
<file alias="ModeSelector.qml">qml/ModeSelector.qml</file>
|
||||
<file alias="AdvancedSetup.qml">qml/AdvancedSetup.qml</file>
|
||||
<file alias="DotIcon.qml">qml/DotIcon.qml</file>
|
||||
<file alias="NodeStatusIcon.qml">qml/NodeStatusIcon.qml</file>
|
||||
<file alias="GuideIcon.qml">qml/GuideIcon.qml</file>
|
||||
<file alias="AdvancedIcon.qml">qml/AdvancedIcon.qml</file>
|
||||
<file alias="UpnpIcon.qml">qml/UpnpIcon.qml</file>
|
||||
<file alias="PortIcon.qml">qml/PortIcon.qml</file>
|
||||
<file>icons/storage.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
2
vendor/logos-storage-module
vendored
2
vendor/logos-storage-module
vendored
@ -1 +1 @@
|
||||
Subproject commit 7dceb6a8dfd5a5bbd47dc9b00e55f80b62ab1e6e
|
||||
Subproject commit b4ecf7a871233608f63b817eeae426f6273695d9
|
||||
Loading…
x
Reference in New Issue
Block a user