Merge pull request #7 from logos-co/feat/onboarding

feat: onboarding
This commit is contained in:
Arnaud 2026-02-17 11:30:04 +04:00 committed by GitHub
commit c7bb9560dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 768 additions and 48 deletions

View File

@ -62,6 +62,23 @@ if(NOT DEFINED LOGOS_STORAGE_ROOT)
endif()
endif()
if(NOT DEFINED LOGOS_DESIGN_SYSTEM_ROOT)
set(_parent_logos_design_system "${CMAKE_SOURCE_DIR}/../logos-design-system")
set(_use_vendor ${LOGOS_STORAGE_UI_USE_VENDOR})
if(NOT _use_vendor)
if(NOT EXISTS "${_parent_logos_design_system}/CMakeLists.txt")
set(_use_vendor ON)
endif()
endif()
if(_use_vendor)
set(LOGOS_DESIGN_SYSTEM_ROOT "${CMAKE_SOURCE_DIR}/vendor/logos-design-system")
else()
set(LOGOS_DESIGN_SYSTEM_ROOT "${_parent_logos_design_system}")
endif()
endif()
set(_liblogos_found FALSE)
if(EXISTS "${LOGOS_LIBLOGOS_ROOT}/src/common/interface.h")
set(_liblogos_found TRUE)
@ -329,6 +346,32 @@ if(absl_FOUND)
)
endif()
# Set up QML module directory for runtime
set(QML_THEME_DIR ${CMAKE_CURRENT_BINARY_DIR}/qml/Logos/Theme)
set(QML_CONTROLS_DIR ${CMAKE_CURRENT_BINARY_DIR}/qml/Logos/Controls)
# Detect if design system is source or installed layout
set(_design_system_is_source FALSE)
if(EXISTS "${LOGOS_DESIGN_SYSTEM_ROOT}/src/qml/theme")
set(_design_system_is_source TRUE)
set(DESIGN_SYSTEM_THEME_SRC "${LOGOS_DESIGN_SYSTEM_ROOT}/src/qml/theme")
set(DESIGN_SYSTEM_CONTROLS_SRC "${LOGOS_DESIGN_SYSTEM_ROOT}/src/qml/controls")
else()
# Installed/Nix layout: files are in lib/Logos/Theme and lib/Logos/Controls
set(DESIGN_SYSTEM_THEME_SRC "${LOGOS_DESIGN_SYSTEM_ROOT}/lib/Logos/Theme")
set(DESIGN_SYSTEM_CONTROLS_SRC "${LOGOS_DESIGN_SYSTEM_ROOT}/lib/Logos/Controls")
endif()
# Copy QML module files to runtime directory
# Pure QML module - no library, just QML files + qmldir
add_custom_command(TARGET storage_ui POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory ${QML_THEME_DIR}
COMMAND ${CMAKE_COMMAND} -E copy_directory ${DESIGN_SYSTEM_THEME_SRC}/ ${QML_THEME_DIR}/
COMMAND ${CMAKE_COMMAND} -E make_directory ${QML_CONTROLS_DIR}
COMMAND ${CMAKE_COMMAND} -E copy_directory ${DESIGN_SYSTEM_CONTROLS_SRC}/ ${QML_CONTROLS_DIR}/
COMMENT "Setting up Logos.Theme and Logos.Controls QML modules for the app"
)
########### LIBRARY DEFINITION END ###########
########### HEADERS ###########

View File

@ -62,7 +62,23 @@ After building the app with `nix build`, you can run it:
./result/bin/logos-storage-ui-app
```
The app will automatically load the required modules (capability_module, storage_module) and the storage_ui Qt plugin. All dependencies are bundled in the Nix store layout.
## Configuration
After onboarding, settings are saved to a file whose location depends on the OS:
| OS | Path |
|---------|--------------------------------------------------|
| Linux | `~/.config/Logos/LogosStorage.conf` |
| macOS | `~/Library/Preferences/Logos.LogosStorage.plist` |
| Windows | `HKCU\Software\Logos\LogosStorage` (Registry) |
## Configuration Management
To restart the onboarding process, simply delete the configuration file and relaunch the application.
You can override the configuration by placing a `config.json` file in the app's startup folder. This file takes precedence over any existing configuration.
The application also provides a JSON editor in the debug panel for runtime configuration tweaks. To apply changes, restart the Storage Module.
#### Nix Organization

View File

@ -29,6 +29,11 @@ int main(int argc, char* argv[]) {
// Create QApplication first
QApplication app(argc, argv);
// Set application properties for Qt Settings
QCoreApplication::setOrganizationName("Logos");
QCoreApplication::setOrganizationDomain("logos.co");
QCoreApplication::setApplicationName("LogosStorage");
// Set the plugins directory
QString pluginsDir = QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../modules");
std::cout << "Setting plugins directory to: " << pluginsDir.toStdString() << std::endl;

53
flake.lock generated
View File

@ -91,7 +91,7 @@
},
"logos-cpp-sdk_3": {
"inputs": {
"nixpkgs": "nixpkgs_3"
"nixpkgs": "nixpkgs_4"
},
"locked": {
"lastModified": 1761230734,
@ -109,7 +109,7 @@
},
"logos-cpp-sdk_4": {
"inputs": {
"nixpkgs": "nixpkgs_4"
"nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1761230734,
@ -127,7 +127,7 @@
},
"logos-cpp-sdk_5": {
"inputs": {
"nixpkgs": "nixpkgs_5"
"nixpkgs": "nixpkgs_6"
},
"locked": {
"lastModified": 1767724329,
@ -145,7 +145,7 @@
},
"logos-cpp-sdk_6": {
"inputs": {
"nixpkgs": "nixpkgs_6"
"nixpkgs": "nixpkgs_7"
},
"locked": {
"lastModified": 1767724329,
@ -161,6 +161,24 @@
"type": "github"
}
},
"logos-design-system": {
"inputs": {
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1771237838,
"narHash": "sha256-UcdHiZ5IdfCSOy17WTz04ZV1n4qqycVfwYzI7BkXeuc=",
"owner": "logos-co",
"repo": "logos-design-system",
"rev": "596811cbb0a0644322267368e87fab80e34203d8",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-design-system",
"type": "github"
}
},
"logos-liblogos": {
"inputs": {
"logos-cpp-sdk": "logos-cpp-sdk",
@ -290,7 +308,7 @@
},
"logos-storage": {
"inputs": {
"nixpkgs": "nixpkgs_7"
"nixpkgs": "nixpkgs_8"
},
"locked": {
"lastModified": 1770982130,
@ -369,16 +387,16 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"lastModified": 1751274312,
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
@ -432,6 +450,22 @@
}
},
"nixpkgs_7": {
"locked": {
"lastModified": 1759036355,
"narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_8": {
"locked": {
"lastModified": 1751274312,
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
@ -451,6 +485,7 @@
"inputs": {
"logos-capability-module": "logos-capability-module",
"logos-cpp-sdk": "logos-cpp-sdk_2",
"logos-design-system": "logos-design-system",
"logos-liblogos": "logos-liblogos_2",
"logos-storage-module": "logos-storage-module",
"nixpkgs": [

View File

@ -10,13 +10,13 @@
logos-storage-module.url = "github:logos-co/logos-storage-module";
#logos-storage-module.url = "path:/home/arnaud/Work/logos/logos-storage-module";
logos-capability-module.url = "github:logos-co/logos-capability-module";
logos-design-system.url = "github:logos-co/logos-design-system";
logos-liblogos.inputs.logos-cpp-sdk.follows = "logos-cpp-sdk";
logos-storage-module.inputs.logos-cpp-sdk.follows = "logos-cpp-sdk";
logos-capability-module.inputs.logos-cpp-sdk.follows = "logos-cpp-sdk";
};
outputs = { self, nixpkgs, logos-cpp-sdk, logos-liblogos, logos-storage-module, logos-capability-module }:
outputs = { self, nixpkgs, logos-cpp-sdk, logos-liblogos, logos-storage-module, logos-capability-module, logos-design-system }:
let
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
@ -25,14 +25,15 @@
logosLiblogos = logos-liblogos.packages.${system}.default;
logosStorageModule = logos-storage-module.packages.${system}.default;
logosCapabilityModule = logos-capability-module.packages.${system}.default;
logosDesignSystem = logos-design-system.packages.${system}.default;
});
in
{
packages = forAllSystems ({ pkgs, logosSdk, logosLiblogos, logosStorageModule, logosCapabilityModule }:
packages = forAllSystems ({ pkgs, logosSdk, logosLiblogos, logosStorageModule, logosCapabilityModule, logosDesignSystem }:
let
# Common configuration
common = import ./nix/default.nix {
inherit pkgs logosSdk logosLiblogos logosStorageModule;
inherit pkgs logosSdk logosLiblogos logosStorageModule logosDesignSystem;
};
src = ./.;
@ -42,8 +43,8 @@
};
# App package
app = import ./nix/app.nix {
inherit pkgs common src logosLiblogos logosSdk logosStorageModule logosCapabilityModule;
app = import ./nix/app.nix {
inherit pkgs common src logosLiblogos logosSdk logosStorageModule logosCapabilityModule logosDesignSystem;
logosStorageUI = lib;
};

View File

@ -1,5 +1,5 @@
# Builds the logos-storage-ui-app standalone application
{ pkgs, common, src, logosLiblogos, logosSdk, logosStorageModule, logosCapabilityModule, logosStorageUI }:
{ pkgs, common, src, logosLiblogos, logosSdk, logosStorageModule, logosCapabilityModule, logosStorageUI, logosDesignSystem }:
pkgs.stdenv.mkDerivation rec {
pname = "${common.pname}-app";
@ -213,6 +213,17 @@ pkgs.stdenv.mkDerivation rec {
cp -L "${logosStorageUI}/lib/storage_ui.$OS_EXT" "$out/"
fi
# Copy QML modules from design system (Logos.Theme and Logos.Controls)
if [ -d "${logosDesignSystem}/lib/Logos" ]; then
echo "Installing QML modules from design system..."
mkdir -p "$out/lib"
cp -r "${logosDesignSystem}/lib/Logos" "$out/lib/"
echo "Installed Logos QML modules:"
ls -la "$out/lib/Logos/"
else
echo "Warning: Logos QML modules not found in design system at ${logosDesignSystem}/lib/Logos"
fi
# Create a README for reference
cat > $out/README.txt <<EOF
Logos Storage UI App - Build Information

View File

@ -1,5 +1,5 @@
# Common build configuration shared across all packages
{ pkgs, logosSdk, logosLiblogos, logosStorageModule }:
{ pkgs, logosSdk, logosLiblogos, logosStorageModule, logosDesignSystem }:
{
pname = "logos-storage-ui";
@ -28,6 +28,7 @@
"-DLOGOS_CPP_SDK_ROOT=${logosSdk}"
"-DLOGOS_LIBLOGOS_ROOT=${logosLiblogos}"
"-DLOGOS_STORAGE_ROOT=${logosStorageModule}"
"-DLOGOS_DESIGN_SYSTEM_ROOT=${logosDesignSystem}"
];
# Environment variables
@ -35,6 +36,7 @@
LOGOS_CPP_SDK_ROOT = "${logosSdk}";
LOGOS_LIBLOGOS_ROOT = "${logosLiblogos}";
LOGOS_STORAGE_ROOT = "${logosStorageModule}";
LOGOS_DESIGN_SYSTEM_ROOT = "${logosDesignSystem}";
};
# Metadata

View File

@ -1,6 +1,7 @@
#include "StorageBackend.h"
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
@ -38,6 +39,10 @@ StorageBackend::~StorageBackend()
LogosResult StorageBackend::init(const QString& configJson = "{}") {
qDebug() << "StorageBackend::initStorage called";
if (configJson != "{}") {
m_configJson = configJson;
}
bool result = m_logos->storage_module.init(m_configJson);
qDebug() << "StorageBackend::initStorage: init";
@ -57,9 +62,11 @@ LogosResult StorageBackend::init(const QString& configJson = "{}") {
QString message = data[1].toString();
setStatus(Stopped);
debug("Failed to start Storage module:" + message);
emit startFailed(message);
} else {
setStatus(Running);
debug("Storage module started.");
emit startCompleted();
}
})) {
qWarning() << "StorageWidget: failed to subscribe to storageStart events";
@ -75,8 +82,9 @@ LogosResult StorageBackend::init(const QString& configJson = "{}") {
} else {
setStatus(Stopped);
debug("Storage module stopped.");
emit stopped();
}
emit stopCompleted();
})) {
qWarning() << "StorageWidget: failed to subscribe to storageStop events";
}
@ -183,10 +191,11 @@ LogosResult StorageBackend::init(const QString& configJson = "{}") {
qWarning() << "StorageWidget: failed to subscribe to storageDownloadProgress events";
}
m_configJson = configJson;
emit configJsonChanged();
debug("config.json content is: " + m_configJson);
if (configJson != "{}") {
m_configJson = configJson;
emit configJsonChanged();
debug("new config is: " + m_configJson);
}
return {true, ""};
}
@ -236,11 +245,13 @@ void StorageBackend::stop() {
if (m_status == StorageStatus::Stopping) {
debug("The Storage Module is already stopping.");
emit stopCompleted();
return;
}
if (m_status != StorageStatus::Running) {
debug("The Storage Module is not started.");
emit stopCompleted();
return;
}
@ -698,6 +709,7 @@ QString StorageBackend::uploadStatus() const { return m_uploadStatus; }
void StorageBackend::reloadIfChanged(const QString& configJson) {
if (configJson == m_configJson) {
debug("No change detected in the config");
return;
}
@ -753,4 +765,60 @@ void StorageBackend::reloadIfChanged(const QString& configJson) {
m_configJson = configJson;
setStatus(StorageStatus::Stopped);
emit configJsonChanged();
}
bool StorageBackend::validateDataDir(const QString& path) {
QFileInfo info(path);
return info.exists() && info.isDir() && info.isReadable() && info.isWritable();
}
QString StorageBackend::buildConfig(const QString& dataDir, int discPort, int tcpPort) {
debug("StorageBackend::updateBasicConfig called with dataDir=" + dataDir);
QJsonDocument doc = QJsonDocument::fromJson(m_configJson.toUtf8());
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);
}
QString StorageBackend::buildConfigFromFile(const QString& path) {
qDebug() << "StorageBackend::buildConfigFromFile called";
QFile file(path);
if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString configJson = QString::fromUtf8(file.readAll());
debug("StorageUIPlugin: config.json is found, configJson=" + configJson);
return configJson;
}
debug("StorageUIPlugin: Failed to load config.json");
return "{}";
}
void StorageBackend::status(StorageStatus status) { m_status = status; }
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
}

View File

@ -1,6 +1,7 @@
#pragma once
#include "logos_api.h"
#include "logos_sdk.h"
#include <QFile>
#include <QObject>
#include <QString>
#include <QStringList>
@ -10,11 +11,24 @@
static const int RET_OK = 0;
static const int RET_PROGRESS = 3;
// Add manual SPR from https://spr.codex.storage/devnet
static const QStringList BOOTSTRAP_NODES = {
"spr:CiUIAhIhA-VlcoiRm02KyIzrcTP-ljFpzTljfBRRKTIvhMIwqBqWEgIDARpJCicAJQgCEiED5WVyiJGbTYrIjOtxM_6WMWnNOWN8FFEpMi-"
"EwjCoGpYQs8n8wQYaCwoJBHTKubmRAnU6GgsKCQR0yrm5kQJ1OipHMEUCIQDwUNsfReB4ty7JFS5WVQ6n1fcko89qVAOfQEHixa03rgIgan2-"
"uFNDT-r4s9TOkLe9YBkCbsRWYCHGGVJ25rLj0QE",
"spr:CiUIAhIhApIj9p6zJDRbw2NoCo-"
"tj98Y760YbppRiEpGIE1yGaMzEgIDARpJCicAJQgCEiECkiP2nrMkNFvDY2gKj62P3xjvrRhumlGISkYgTXIZozMQvcz8wQYaCwoJBAWhF3WRAnVEG"
"gsKCQQFoRd1kQJ1RCpGMEQCIFZB84O_nzPNuViqEGRL1vJTjHBJ-i5ZDgFL5XZxm4HAAiB8rbLHkUdFfWdiOmlencYVn0noSMRHzn4lJYoShuVzlw",
"spr:CiUIAhIhApqRgeWRPSXocTS9RFkQmwTZRG-"
"Cdt7UR2N7POoz606ZEgIDARpJCicAJQgCEiECmpGB5ZE9JehxNL1EWRCbBNlEb4J23tRHY3s86jPrTpkQj8_"
"8wQYaCwoJBAXfEfiRAnVOGgsKCQQF3xH4kQJ1TipGMEQCIGWJMsF57N1iIEQgTH7IrVOgEgv0J2P2v3jvQr5Cjy-RAiAy4aiZ8QtyDvCfl_K_"
"w6SyZ9csFGkRNTpirq_M_QNgKw"};
class StorageBackend : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString debugLogs READ debugLogs NOTIFY debugLogsChanged)
Q_PROPERTY(StorageStatus status READ status NOTIFY statusChanged)
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)
@ -31,6 +45,8 @@ class StorageBackend : public QObject {
int uploadProgress() const;
QString uploadStatus() const;
Q_INVOKABLE static QString defaultDataDir();
explicit StorageBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
~StorageBackend();
@ -54,13 +70,20 @@ class StorageBackend : public QObject {
void downloadManifest(const QString& cid);
void downloadManifests();
void space();
bool validateDataDir(const QString& path);
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);
signals:
void startCompleted();
void startFailed(const QString& error);
void statusChanged();
void debugLogsChanged();
void stopped();
void stopCompleted();
void cidChanged();
void configJsonChanged();
void uploadProgressChanged();
@ -72,7 +95,6 @@ class StorageBackend : public QObject {
void setStatus(StorageStatus newStatus);
void peerConnect(const QString& peerId);
void debug(const QString& log);
void reloadIfChanged(const QString& configJson);
LogosAPI* m_logosAPI;
LogosModules* m_logos;

View File

@ -1,5 +1,6 @@
#include "StorageUIPlugin.h"
#include "StorageBackend.h"
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
@ -7,14 +8,46 @@
#include <QQmlEngine>
#include <QQuickItem>
#include <QQuickWidget>
#include <QSettings>
QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
qDebug() << "StorageUIPlugin::createWidget called";
QCoreApplication::setOrganizationName("Logos");
QCoreApplication::setOrganizationDomain("logos.co");
QCoreApplication::setApplicationName("LogosStorage");
QQuickWidget* quickWidget = new QQuickWidget();
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
QString qmlPath = "qrc:/StorageView.qml";
// Add import path for Logos QML modules (Logos.Theme, Logos.Controls)
QQmlEngine* engine = quickWidget->engine();
QString qmlModulesPath = QCoreApplication::applicationDirPath() + "/../lib";
engine->addImportPath(qmlModulesPath);
qDebug() << "StorageUIPlugin: Loading settings...";
QSettings settings("Logos", "LogosStorage");
int discoveryPort = settings.value("Storage/discoveryPort", 0).toInt();
int tcpPort = settings.value("Storage/tcpPort", 0).toInt();
QString dataDir = settings.value("Storage/dataDir", "").toString();
bool onboardingCompleted = settings.value("Storage/onboardingCompleted", false).toBool();
qDebug() << "StorageUIPlugin: Settings Loaded onboardingCompleted=" << onboardingCompleted;
qDebug() << "StorageUIPlugin: Settings Loaded dataDir=" << dataDir;
qDebug() << "StorageUIPlugin: Settings Loaded discoveryPort=" << discoveryPort;
qDebug() << "StorageUIPlugin: Settings Loaded tcpPort=" << tcpPort;
QString qmlPath = "qrc:/Main.qml";
// Create backend instance
StorageBackend* backend = new StorageBackend(logosAPI, quickWidget);
if (onboardingCompleted) {
qmlPath = "qrc:/StorageView.qml";
}
qDebug() << "StorageUIPlugin: qmlPath=" << qmlPath;
quickWidget->setSource(QUrl(qmlPath));
@ -22,41 +55,38 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) {
qWarning() << "StorageUIPlugin: Failed to load QML:" << quickWidget->errors();
}
// Create backend instance
StorageBackend* backend = new StorageBackend(logosAPI, quickWidget);
// Set backend as context property
QQuickItem* root = quickWidget->rootObject();
Q_ASSERT(root);
root->setProperty("backend", QVariant::fromValue(static_cast<QObject*>(backend)));
QFileInfo info("config.json");
QString configJson = "{}";
if (info.exists() && info.isFile()) {
qDebug() << "StorageUIPlugin: config.json is found, let's try to load it...";
QFile file("config.json");
if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) {
configJson = QString::fromUtf8(file.readAll());
qDebug() << "StorageUIPlugin: config.json is found, let's try to load it... configJson=" << configJson;
} else {
qDebug() << "StorageUIPlugin: Failed to load config.json";
}
if (onboardingCompleted) {
configJson = backend->buildConfig(dataDir, discoveryPort, tcpPort);
}
QFileInfo info("config.json");
if (info.exists() && info.isFile()) {
qWarning()
<< "StorageUIPlugin: config.json is found ! It will override the configuration loaded by the onboarding !";
configJson = backend->buildConfigFromFile("config.json");
}
qDebug() << "StorageUIPlugin: configuration loaded configLoaded=" << configJson;
LogosResult result = backend->init(configJson);
if (!result.success) {
QString error = result.getError();
qWarning() << "StorageUIPlugin: Failed to init backend, will use mock version:" << error;
} else {
result = backend->start();
} else if (onboardingCompleted) {
LogosResult result = backend->start();
if (!result.success) {
qWarning() << "StorageUIPlugin: Failed to init backend, will use mock version:" << result.getError();
qWarning() << "StorageUIPlugin: Failed to start the Storage Module.";
}
}
@ -126,7 +156,7 @@ void StorageUIPlugin::destroyWidget(QWidget* widget) {
});
// Connect to stop signal
QObject::connect(backend, &StorageBackend::stopped, &loop, [&]() { loop.quit(); }, Qt::QueuedConnection);
QObject::connect(backend, &StorageBackend::stopCompleted, &loop, [&]() { loop.quit(); }, Qt::QueuedConnection);
// Call the stop method asynchronously
QMetaObject::invokeMethod(backend, "stop", Qt::QueuedConnection);

View File

@ -34,6 +34,23 @@ if(NOT DEFINED LOGOS_CPP_SDK_ROOT)
endif()
endif()
if(NOT DEFINED LOGOS_DESIGN_SYSTEM_ROOT)
set(_parent_logos_design_system "${CMAKE_SOURCE_DIR}/../../../logos-design-system")
set(_use_vendor ${LOGOS_STORAGE_UI_USE_VENDOR})
if(NOT _use_vendor)
if(NOT EXISTS "${_parent_logos_design_system}/CMakeLists.txt")
set(_use_vendor ON)
endif()
endif()
if(_use_vendor)
set(LOGOS_DESIGN_SYSTEM_ROOT "${CMAKE_SOURCE_DIR}/../../vendor/logos-design-system")
else()
set(LOGOS_DESIGN_SYSTEM_ROOT "${_parent_logos_design_system}")
endif()
endif()
# Locate the logos_api header file.
# If the file is found, the sdk is considered found.
set(_cpp_sdk_found FALSE)
@ -110,6 +127,26 @@ qt_add_qml_module(appqml
../StorageBackend.h
QML_FILES
StorageView.qml
OnBoarding.qml
Main.qml
StartNode.qml
LogosTextField.qml
LogosStorageButton.qml
)
# Set up QML module directory for runtime
set(DESIGN_SYSTEM_QML ${LOGOS_DESIGN_SYSTEM_ROOT}/src/qml)
set(QML_THEME_DIR ${CMAKE_CURRENT_BINARY_DIR}/qml/Logos/Theme)
set(QML_CONTROLS_DIR ${CMAKE_CURRENT_BINARY_DIR}/qml/Logos/Controls)
# Copy QML module files to runtime directory
# Pure QML module - no library, just QML files + qmldir
add_custom_command(TARGET storage_generated POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory ${QML_THEME_DIR}
COMMAND ${CMAKE_COMMAND} -E copy_directory ${DESIGN_SYSTEM_QML}/theme/ ${QML_THEME_DIR}/
COMMAND ${CMAKE_COMMAND} -E make_directory ${QML_CONTROLS_DIR}
COMMAND ${CMAKE_COMMAND} -E copy_directory ${DESIGN_SYSTEM_QML}/controls/ ${QML_CONTROLS_DIR}/
COMMENT "Setting up Logos.Theme and Logos.Controls QML modules for the app"
)
########### QML DEFINITION END ###########

View File

@ -0,0 +1,39 @@
import QtQuick
import QtQuick.Controls
import Logos.Theme
Button {
id: control
padding: Theme.spacing.small
contentItem: Text {
text: control.text
font.pixelSize: Theme.typography.primaryText
color: control.enabled ? Theme.palette.text : Theme.palette.textMuted
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: {
if (!control.enabled)
return Theme.palette.backgroundElevated
if (control.hovered)
return Theme.palette.backgroundTertiary
return Theme.palette.backgroundSecondary
}
border.width: 1
border.color: Theme.palette.border
radius: Theme.spacing.tiny
Behavior on color {
ColorAnimation {
duration: 150
}
}
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
}

View File

@ -0,0 +1,27 @@
import QtQuick
import QtQuick.Controls
import Logos.Theme
TextField {
id: root
property bool isValid: acceptableInput && text.length > 0
placeholderTextColor: Theme.palette.textPlaceholder
color: isValid ? Theme.palette.text : Theme.palette.error
selectByMouse: true
background: Rectangle {
Rectangle {
anchors.fill: parent
color: Theme.palette.backgroundSecondary
}
// Border bottom
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: 1
color: root.isValid ? Theme.palette.textMuted : Theme.palette.error
}
}
}

136
src/qml/Main.qml Normal file
View File

@ -0,0 +1,136 @@
import QtQuick
import QtQuick.Controls
import QtCore
import Logos.Theme
import Logos.Controls
// qmllint disable unqualified
Item {
id: root
implicitWidth: 600
implicitHeight: 400
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
property int status
signal startCompleted
signal startFailed
signal stopCompleted
function updateBasicConfig(dataDir, discPort) {
console.log("updateBasicConfig", dataDir, discPort)
}
function start() {
timer.start()
}
function stop() {
root.backend.stopCompleted()
}
function defaultDataDir() {
return ".cache/storage"
}
function buildConfig() {}
function reloadIfChanged() {}
function init() {}
}
Settings {
id: settings
category: "Storage"
property int discoveryPort: 0
property int tcpPort: 0
property string dataDir: ""
property bool onboardingCompleted: false
Component.onCompleted: {
if (onboardingCompleted) {
// stackView.replace(storageView)
// root.backend.start();
}
}
}
StackView {
id: stackView
anchors.fill: parent
initialItem: onboarding
}
Component {
id: onboarding
OnBoarding {
id: onboardingInstance
onCompleted: {
settings.discoveryPort = discoveryPort
settings.dataDir = dataDir
settings.tcpPort = tcpPort
settings.onboardingCompleted = true
let config = root.backend.buildConfig(dataDir,
discoveryPort, tcpPort)
root.backend.reloadIfChanged(config)
root.backend.start()
stackView.push(startNodeView)
}
}
}
Component {
id: storageView
StorageView {
backend: root.backend // @disable-check M228
}
}
Component {
id: startNodeView
StartNode {
backend: root.backend
onBack: {
root.backend.stop()
}
onNext: {
stackView.push(storageView)
}
}
}
Connections {
target: root.backend
function onStopCompleted() {
stackView.pop()
}
}
}

154
src/qml/OnBoarding.qml Normal file
View File

@ -0,0 +1,154 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Dialogs
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
Rectangle {
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
function validateDataDir(path) {
return path != "error"
}
function defaultDataDir() {
return ".cache/storage"
}
}
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
}
ColumnLayout {
id: discoveryPortColumn
spacing: Theme.spacing.tiny
Layout.fillWidth: true
LogosText {
text: "Discovery port"
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.text
}
LogosTextField {
isValid: acceptableInput && text.length > 0
id: discoveryPortTextField
placeholderText: "Enter the discovery port"
text: root.discoveryPort
validator: IntValidator {
bottom: 1
top: 65535
}
onTextChanged: {
if (isValid) {
root.discoveryPort = parseInt(text)
}
}
}
}
ColumnLayout {
id: tcpPortColumn
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)
}
}
}
}
ColumnLayout {
spacing: Theme.spacing.tiny
Layout.fillWidth: true
LogosText {
text: "Data dir"
}
RowLayout {
spacing: Theme.spacing.tiny
LogosTextField {
id: dataDirTextField
placeholderText: "Enter the data dir"
text: root.dataDir
Layout.fillWidth: true
onTextChanged: {
if (text.length > 0) {
isValid = root.backend.validateDataDir(text)
} else {
isValid = false
}
root.dataDir = text
}
}
LogosStorageButton {
text: "Choose"
onClicked: folderDialog.open()
}
}
FolderDialog {
id: folderDialog
onAccepted: {
dataDirTextField.text = selectedFolder
}
}
}
}
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()
}
}

78
src/qml/StartNode.qml Normal file
View File

@ -0,0 +1,78 @@
import QtQuick
import QtQuick.Layouts
import Logos.Controls
import Logos.Theme
Rectangle {
id: root
color: Theme.palette.background
Layout.fillWidth: true
Layout.fillHeight: true
implicitWidth: 600
implicitHeight: 400
property var backend
property string status: ""
property bool starting: true
property bool success: false
signal back
signal next
Connections {
target: root.backend
function onStartCompleted() {
console.log("onStartCompleted received")
root.starting = false
root.status = "Logos Storage started successfully."
root.success = true
}
function onStartFailed(error) {
console.log("onStartFailed received")
root.starting = false
root.status = "Failed to start: " + error
}
}
ColumnLayout {
anchors.centerIn: parent
spacing: Theme.spacing.medium
width: 400
LogosText {
id: titleText
font.pixelSize: Theme.typography.titleText
text: "Starting your node...."
Layout.alignment: Qt.AlignHCenter
}
LogosText {
id: statusText
font.pixelSize: Theme.typography.primaryText
text: root.status
Layout.alignment: Qt.AlignHCenter
}
}
LogosStorageButton {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.bottomMargin: 10
anchors.leftMargin: 10
text: "Back"
onClicked: root.back()
enabled: root.starting == false
}
LogosStorageButton {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.bottomMargin: 10
anchors.rightMargin: 10
text: "Next"
onClicked: root.next()
enabled: root.success == true
}
}

View File

@ -5,8 +5,10 @@ import QtQuick.Layouts
Rectangle {
id: root
width: 400
height: 700
Layout.fillWidth: true
Layout.fillHeight: true
implicitWidth: 600
implicitHeight: 400
color: "#000000"
property var backend: mockBackend
@ -538,6 +540,7 @@ Rectangle {
color: "#d4d4d4"
width: parent.width
height: parent.height
wrapMode: Text.WrapAnywhere
background: Rectangle {
color: "#1e1e1e"

View File

@ -5,10 +5,18 @@
int main(int argc, char* argv[]) {
QGuiApplication app(argc, argv);
QCoreApplication::setOrganizationName("Logos");
QCoreApplication::setOrganizationDomain("logos.co");
QCoreApplication::setApplicationName("LogosStorage");
QQmlApplicationEngine engine;
QObject::connect(
&engine, &QQmlApplicationEngine::objectCreationFailed, &app, []() { QCoreApplication::exit(-1); },
Qt::QueuedConnection);
engine.addImportPath(QCoreApplication::applicationDirPath() + "/qml");
engine.loadFromModule("StorageBackend", "OnBoarding");
return app.exec();
}

View File

@ -1,5 +1,10 @@
<RCC>
<qresource prefix="/">
<file alias="Main.qml">qml/Main.qml</file>
<file alias="StartNode.qml">qml/StartNode.qml</file>
<file alias="OnBoarding.qml">qml/OnBoarding.qml</file>
<file alias="StorageView.qml">qml/StorageView.qml</file>
<file alias="LogosTextField.qml">qml/LogosTextField.qml</file>
<file alias="LogosStorageButton.qml">qml/LogosStorageButton.qml</file>
</qresource>
</RCC>