diff --git a/CMakeLists.txt b/CMakeLists.txt index c4ddce7..9970423 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 ########### diff --git a/README.md b/README.md index cb04452..b5dab98 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/main.cpp b/app/main.cpp index e3336bc..71883d7 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -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; diff --git a/flake.lock b/flake.lock index ac18b22..34b51c9 100644 --- a/flake.lock +++ b/flake.lock @@ -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": [ diff --git a/flake.nix b/flake.nix index 89a364a..280c45c 100644 --- a/flake.nix +++ b/flake.nix @@ -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; }; diff --git a/nix/app.nix b/nix/app.nix index 76aa804..f70e55a 100644 --- a/nix/app.nix +++ b/nix/app.nix @@ -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 < #include +#include #include #include #include @@ -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 } diff --git a/src/StorageBackend.h b/src/StorageBackend.h index 859da3f..a920854 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -1,6 +1,7 @@ #pragma once #include "logos_api.h" #include "logos_sdk.h" +#include #include #include #include @@ -10,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; diff --git a/src/StorageUIPlugin.cpp b/src/StorageUIPlugin.cpp index 6ab6e03..89acd7c 100644 --- a/src/StorageUIPlugin.cpp +++ b/src/StorageUIPlugin.cpp @@ -1,5 +1,6 @@ #include "StorageUIPlugin.h" #include "StorageBackend.h" +#include #include #include #include @@ -7,14 +8,46 @@ #include #include #include +#include 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(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); diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index 1719efd..a3073a4 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -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 ########### diff --git a/src/qml/LogosStorageButton.qml b/src/qml/LogosStorageButton.qml new file mode 100644 index 0000000..0ecf2f0 --- /dev/null +++ b/src/qml/LogosStorageButton.qml @@ -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 + } +} diff --git a/src/qml/LogosTextField.qml b/src/qml/LogosTextField.qml new file mode 100644 index 0000000..63cd9d5 --- /dev/null +++ b/src/qml/LogosTextField.qml @@ -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 + } + } +} diff --git a/src/qml/Main.qml b/src/qml/Main.qml new file mode 100644 index 0000000..93317cd --- /dev/null +++ b/src/qml/Main.qml @@ -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() + } + } +} diff --git a/src/qml/OnBoarding.qml b/src/qml/OnBoarding.qml new file mode 100644 index 0000000..ad97fc9 --- /dev/null +++ b/src/qml/OnBoarding.qml @@ -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() + } +} diff --git a/src/qml/StartNode.qml b/src/qml/StartNode.qml new file mode 100644 index 0000000..f218c97 --- /dev/null +++ b/src/qml/StartNode.qml @@ -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 + } +} diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index dd54af4..1b95f16 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -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" diff --git a/src/qml/main.cpp b/src/qml/main.cpp index c930aa9..1a59950 100644 --- a/src/qml/main.cpp +++ b/src/qml/main.cpp @@ -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(); } diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index 6b04df3..0ec01ad 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -1,5 +1,10 @@ + qml/Main.qml + qml/StartNode.qml + qml/OnBoarding.qml qml/StorageView.qml + qml/LogosTextField.qml + qml/LogosStorageButton.qml