From b25f14210ffcc49dcac45e9fe13b28edec459ac8 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 22 Feb 2026 16:37:27 +0400 Subject: [PATCH] Update a lot of changes --- src/StorageBackend.cpp | 200 +++++++-------- src/StorageBackend.h | 27 +- src/qml/AdvancedSetup.qml | 48 +--- src/qml/CMakeLists.txt | 29 ++- src/qml/DeleteIcon.qml | 10 +- src/qml/DiskWidget.qml | 96 ++++--- src/qml/DotIcon.qml | 14 +- src/qml/HealthIndicator.qml | 79 ++---- src/qml/JsonEditor.qml | 4 + src/qml/LogosStorageButton.qml | 13 +- src/qml/LogosTextField.qml | 4 + src/qml/Main.qml | 12 - src/qml/ManifestTable.qml | 176 +++++++++---- src/qml/ModeSelector.qml | 16 +- src/qml/NodeStatusIcon.qml | 21 +- src/qml/OnBoarding.qml | 16 +- src/qml/PeersWidget.qml | 63 +---- src/qml/SettingsIcon.qml | 15 +- src/qml/SettingsPopup.qml | 55 +--- src/qml/StorageView.qml | 444 ++------------------------------- src/qml/UploadWidget.qml | 30 ++- src/storage_resources.qrc | 6 +- vendor/logos-design-system | 2 +- 23 files changed, 446 insertions(+), 934 deletions(-) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index 494e0dc..bd860b6 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -80,7 +80,10 @@ LogosResult StorageBackend::init(const QString& configJson) { setStatus(Running); debug("Storage module started."); // QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); - // QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); + + QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &StorageBackend::tryDebug, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); emit startCompleted(); } })) { @@ -104,20 +107,6 @@ LogosResult StorageBackend::init(const QString& configJson) { qWarning() << "StorageWidget: failed to subscribe to storageStop events"; } - if (!m_logos->storage_module.on("storageConnect", [this](const QVariantList& data) { - bool success = data[0].toBool(); - - if (!success) { - QString message = data[1].toString(); - debug("Failed to connect: " + message); - } else { - // TODO add the peer id - debug("Successfully connected to peer."); - } - })) { - qWarning() << "StorageWidget: failed to subscribe to storageConnect events"; - } - if (!m_logos->storage_module.on("storageUploadProgress", [this](const QVariantList& data) { bool success = data[0].toBool(); @@ -161,17 +150,17 @@ LogosResult StorageBackend::init(const QString& configJson) { emit uploadStatusChanged(); } else { QString sessionId = data[1].toString(); - m_cid = data[2].toString(); - emit cidChanged(); - debug("Upload completed for session " + sessionId + " with CID " + m_cid); + QString cid = data[2].toString(); + debug("Upload completed for session " + sessionId + " with CID " + cid); // Complète la progress bar m_uploadProgress = 100; m_uploadStatus = "Upload completed!"; emit uploadProgressChanged(); emit uploadStatusChanged(); - + emit uploadCompleted(cid); QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); } })) { qWarning() << "StorageWidget: failed to subscribe to storageUploadProgress events"; @@ -200,9 +189,11 @@ LogosResult StorageBackend::init(const QString& configJson) { debug("Failed to download: " + message); } else { QString sessionId = data[1].toString(); - m_cid = data[2].toString(); - emit cidChanged(); - debug("Download completed for session " + sessionId + " with CID " + m_cid); + QString cid = data[2].toString(); + emit downloadCompleted(data[2].toString()); + + QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); + debug("Download completed for session " + sessionId + " with CID " + cid); } })) { qWarning() << "StorageWidget: failed to subscribe to storageDownloadProgress events"; @@ -321,8 +312,32 @@ void StorageBackend::debug(const QString& log) { void StorageBackend::tryDebug() { auto result = m_logos->storage_module.debug(); - debug("Debug " + result.getString()); + debug("Peer ID: " + result.getString("id")); + debug("SPR: " + result.getString("spr")); + + QStringList addrs = result.getValue("addrs"); + for (const QString& addr : addrs) { + debug("Listen address: " + addr); + } + + QStringList announceAddresses = result.getValue("announceAddresses"); + for (const QString& addr : announceAddresses) { + debug("Announce address: " + addr); + } + + QVariantMap table = result.getValue("table"); + QVariantList nodes = table["nodes"].toList(); + + for (const QVariant& nodeVar : nodes) { + QVariantMap node = nodeVar.toMap(); + QString peerId = node["peerId"].toString(); + bool seen = node["seen"].toBool(); + debug("Peer found, peerId=" + peerId + ", seen=" + (seen ? "true" : "false")); + } + + emit peersUpdated(nodes.size()); } + void StorageBackend::tryPeerConnect(const QString& peerId) { qDebug().noquote() << "StorageBackend: tryPeerConnect called with peerId=" << peerId; @@ -565,26 +580,30 @@ void StorageBackend::exists(const QString& cid) { } void StorageBackend::remove(const QString& cid) { - qDebug() << "StorageBackend::remove called"; + qDebug() << "StorageBackend::remove called with cid=" << cid; - LogosResult result = m_logos->storage_module.remove(cid); + LogosResult result = m_logos->storage_module.exists(cid); if (!result.success) { - // Log but continue — manifest might not have local data, remove it from the list anyway - debug("StorageBackend::remove: storage returned error=" + result.getError() + " (removing from list regardless)"); - } else { - debug("Cid " + cid + " removed from storage."); + debug("StorageBackend::remove failed to check exists: " + result.getError()); + emit error("Failed to check exists " + cid + ": " + result.getError()); + return; } - // Always remove from manifests list - for (int i = 0; i < m_manifests.size(); ++i) { - if (m_manifests[i].toMap().value("cid").toString() == cid) { - m_manifests.removeAt(i); - emit manifestsChanged(); - break; - } + if (!result.getBool()) { + debug("StorageBackend::remove blocks don't exist in store."); + return; } + result = m_logos->storage_module.remove(cid); + if (!result.success) { + debug("StorageBackend::remove failed: " + result.getError()); + emit error("Failed to remove " + cid + ": " + result.getError()); + return; + } + + debug("Cid " + cid + " removed from local storage."); + QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); } @@ -675,21 +694,15 @@ void StorageBackend::downloadManifest(const QString& cid) { debug("Manifest filename: " + filename); debug("Manifest mimetype: " + mimetype); - // Add to manifests list QVariantMap manifest; - manifest["cid"] = cid; - manifest["treeCid"] = treeCid; - manifest["filename"] = filename; - manifest["mimetype"] = mimetype; + manifest["cid"] = cid; + manifest["treeCid"] = treeCid; + manifest["filename"] = filename; + manifest["mimetype"] = mimetype; manifest["datasetSize"] = datasetSize; - manifest["blockSize"] = blockSize; - - m_manifests.append(manifest); - emit manifestsChanged(); + manifest["blockSize"] = blockSize; } -QVariantList StorageBackend::manifests() const { return m_manifests; } - void StorageBackend::downloadManifests() { qDebug() << "StorageBackend::downloadManifests called"; @@ -700,27 +713,9 @@ void StorageBackend::downloadManifests() { return; } - QVariantList manifestsList = result.getList(); - int count = manifestsList.size(); - debug(QString("Found %1 manifests").arg(count)); + qDebug() << "StorageBackend::downloadManifests called, size=" << result.getList().size(); - m_manifests.clear(); - - for (const QVariant& manifestVariant : manifestsList) { - QVariantMap src = manifestVariant.toMap(); - - QVariantMap manifest; - manifest["cid"] = src.value("cid").toString(); - manifest["treeCid"] = src.value("treeCid").toString(); - manifest["filename"] = src.value("filename").toString(); - manifest["mimetype"] = src.value("mimetype").toString(); - manifest["datasetSize"] = src.value("datasetSize").toLongLong(); - manifest["blockSize"] = src.value("blockSize").toLongLong(); - - m_manifests.append(manifest); - } - - emit manifestsChanged(); + emit manifestsUpdated(result.getList()); } void StorageBackend::space() { @@ -729,31 +724,18 @@ void StorageBackend::space() { LogosResult result = m_logos->storage_module.space(); if (!result.success) { - debug("StorageBackend::space failed with error=" + result.getError()); + debug("Space failed with error=" + result.getError()); return; } - qDebug() << "StorageBackend::space raw value:" << result.value; + const qlonglong total = result.getValue("quotaMaxBytes"); + const qlonglong used = + result.getValue("quotaUsedBytes") + result.getValue("quotaReservedBytes"); - static constexpr qint64 DEFAULT_QUOTA = 20LL * 1024 * 1024 * 1024; // 20 GB - - // Check config for a quota-max-bytes override - 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"); - m_quotaReservedBytes = result.getInt("quotaReservedBytes"); - emit quotaChanged(); - - debug(QString("Space totalBlocks %1").arg(result.getInt("totalBlocks"))); - debug(QString("Space quotaMaxBytes %1").arg(m_quotaMaxBytes)); - debug(QString("Space quotaUsedBytes %1").arg(m_quotaUsedBytes)); - debug(QString("Space quotaReservedBytes %1").arg(m_quotaReservedBytes)); + emit spaceUpdated(total, used); } -qint64 StorageBackend::quotaMaxBytes() const { return m_quotaMaxBytes; } -qint64 StorageBackend::quotaUsedBytes() const { return m_quotaUsedBytes; } -qint64 StorageBackend::quotaReservedBytes() const { return m_quotaReservedBytes; } +QString StorageBackend::configJson() const { return QString::fromUtf8(m_config.toJson(QJsonDocument::Indented)); } void StorageBackend::updateLogLevel(const QString& logLevel) { qDebug() << "StorageBackend::updateLogLevel called with logLevel=" << logLevel; @@ -770,8 +752,6 @@ void StorageBackend::updateLogLevel(const QString& logLevel) { StorageBackend::StorageStatus StorageBackend::status() const { return m_status; } -QString StorageBackend::cid() const { return m_cid; } - int StorageBackend::uploadProgress() const { return m_uploadProgress; } QString StorageBackend::uploadStatus() const { return m_uploadStatus; } @@ -822,7 +802,7 @@ void StorageBackend::reloadIfChanged(const QString& configJson) { void StorageBackend::saveCurrentConfig() { qDebug() << "StorageBackend::saveUserConfig"; - saveUserConfig(QString::fromUtf8(m_config.toJson(QJsonDocument::Indented))); + saveUserConfig(configJson()); } void StorageBackend::saveUserConfig(const QString& configJson) { @@ -929,58 +909,56 @@ void StorageBackend::checkNodeIsUp() { debug("DHT seems okay, found peers"); - // Extract TCP ports from announceAddresses. + // Extract IP+port pairs from announceAddresses. // Format: "/ip4/1.2.3.4/tcp/PORT" QVariantList announceAddresses = result.getValue("announceAddresses"); - QList ports; + QList> endpoints; 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); - } + const QStringList parts = addr.toString().split("/"); + // ["", "ip4", "1.2.3.4", "tcp", "8079"] + const int tcpIndex = parts.indexOf("tcp"); + if (tcpIndex >= 1 && tcpIndex + 1 < parts.size()) { + const QString ip = parts[tcpIndex - 1]; + const int port = parts[tcpIndex + 1].toInt(); + if (port > 0 && !ip.isEmpty()) + endpoints.append({ ip, 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 (endpoints.isEmpty()) { + debug("No TCP endpoints found in announce addresses"); 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. " + emit nodeIsntUp("No announced addresses found. Your TCP port is probably incorrect. " "Try going back and check your port forwarding configuration."); } - return; } - debug(QString("Checking reachability for %1 port(s)...").arg(ports.size())); + debug(QString("Checking reachability for %1 endpoint(s)...").arg(endpoints.size())); - // Check each port via the echo service, one by one. bool foundReachable = false; - for (int port : ports) { + for (const auto& [ip, port] : endpoints) { QNetworkAccessManager manager; - QNetworkRequest request(QUrl(QString("%1/port/%2").arg(ECHO_PROVIDER).arg(port))); - QNetworkReply* reply = manager.get(request); + const QUrl url(QString("%1/%2/%3").arg(PORT_CHECKER_PROVIDER).arg(ip).arg(port)); + QNetworkReply* reply = manager.get(QNetworkRequest(url)); 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")); + const bool reachable = reply->readAll() == "True"; + debug(QString("%1:%2 is %3").arg(ip).arg(port).arg(reachable ? "reachable" : "not reachable")); if (reachable) { foundReachable = true; } } else { - debug("Port check failed for port " + QString::number(port) + ": " + reply->errorString()); + debug(QString("Port check failed for %1:%2 : %3").arg(ip).arg(port).arg(reply->errorString())); } reply->deleteLater(); diff --git a/src/StorageBackend.h b/src/StorageBackend.h index 06f8eab..ca8afc8 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -12,6 +12,7 @@ static const int RET_OK = 0; static const int RET_PROGRESS = 3; static const QString ECHO_PROVIDER = "https://echo.codex.storage/"; +static const QString PORT_CHECKER_PROVIDER = "https://portchecker.io/api/"; 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"; @@ -34,14 +35,8 @@ class StorageBackend : public QObject { QML_ELEMENT 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(int uploadProgress READ uploadProgress NOTIFY uploadProgressChanged) Q_PROPERTY(QString uploadStatus READ uploadStatus NOTIFY uploadStatusChanged) - Q_PROPERTY(QVariantList manifests READ manifests NOTIFY manifestsChanged) - Q_PROPERTY(qint64 quotaMaxBytes READ quotaMaxBytes NOTIFY quotaChanged) - Q_PROPERTY(qint64 quotaUsedBytes READ quotaUsedBytes NOTIFY quotaChanged) - Q_PROPERTY(qint64 quotaReservedBytes READ quotaReservedBytes NOTIFY quotaChanged) - public: enum StorageStatus { // Stopped means that the context is created but the module is not started @@ -59,15 +54,11 @@ class StorageBackend : public QObject { }; Q_ENUM(StorageStatus) - QString cid() const; QString debugLogs() const; StorageStatus status() const; int uploadProgress() const; QString uploadStatus() const; - QVariantList manifests() const; - qint64 quotaMaxBytes() const; - qint64 quotaUsedBytes() const; - qint64 quotaReservedBytes() const; + Q_INVOKABLE QString configJson() const; static QJsonDocument defaultConfig(); @@ -153,14 +144,16 @@ class StorageBackend : public QObject { void statusChanged(); void debugLogsChanged(); void stopCompleted(); - void cidChanged(); void uploadProgressChanged(); void uploadStatusChanged(); - void manifestsChanged(); + void manifestsUpdated(const QVariantList& manifests); void quotaChanged(); void initCompleted(); void natExtConfigCompleted(); + void uploadCompleted(const QString& cid); + void downloadCompleted(const QString& cid); void error(const QString& message); + void spaceUpdated(qlonglong total, qlonglong used); // Emitted when the node port is reachable from the internet void nodeIsUp(); @@ -168,6 +161,9 @@ class StorageBackend : public QObject { // Emitted when the node port is not reachable, with a reason void nodeIsntUp(const QString& reason); + // Emitted when the peer count changes (from checkNodeIsUp) + void peersUpdated(int count); + private slots: private: @@ -180,14 +176,9 @@ class StorageBackend : public QObject { LogosModules* m_logos; StorageStatus m_status; QString m_debugLogs; - QString m_cid; int m_uploadProgress = 0; QString m_uploadStatus = ""; qint64 m_uploadTotalBytes = 0; qint64 m_uploadedBytes = 0; - QVariantList m_manifests; - qint64 m_quotaMaxBytes = 0; - qint64 m_quotaUsedBytes = 0; - qint64 m_quotaReservedBytes = 0; QJsonDocument m_config; }; diff --git a/src/qml/AdvancedSetup.qml b/src/qml/AdvancedSetup.qml index 8e23e22..502a8c1 100644 --- a/src/qml/AdvancedSetup.qml +++ b/src/qml/AdvancedSetup.qml @@ -32,50 +32,13 @@ LogosStorageLayout { Layout.fillWidth: true } - // ── JSON editor ────────────────────────────────────────────────── - Rectangle { + JsonEditor { + id: jsonEditor 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() - } - } + Component.onCompleted: load(root.backend.configJson() || "{}") } - // ── Buttons ────────────────────────────────────────────────────── RowLayout { Layout.alignment: Qt.AlignHCenter spacing: Theme.spacing.medium @@ -88,10 +51,9 @@ LogosStorageLayout { LogosStorageButton { text: "Validate" variant: "success" - enabled: jsonArea.isValid + enabled: jsonEditor.isValid onClicked: { - root.backend.saveUserConfig(jsonArea.text) - root.backend.reloadIfChanged(jsonArea.text) + root.backend.saveUserConfig(jsonEditor.text) root.completed() } } diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index a96ce58..2e73910 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -138,12 +138,29 @@ qt_add_qml_module(appqml HealthIndicator.qml ModeSelector.qml AdvancedSetup.qml - DotIcon.qml - NodeStatusIcon.qml - GuideIcon.qml - AdvancedIcon.qml - UpnpIcon.qml - PortIcon.qml + icons/DotIcon.qml + icons/NodeStatusIcon.qml + icons/GuideIcon.qml + icons/AdvancedIcon.qml + icons/UpnpIcon.qml + icons/PortIcon.qml + icons/StorageIcon.qml + icons/PlayIcon.qml + icons/StopIcon.qml + icons/SettingsIcon.qml + icons/UploadIcon.qml + icons/DownloadIcon.qml + icons/DeleteIcon.qml + icons/ArcWidget.qml + DiskWidget.qml + UploadWidget.qml + PeersWidget.qml + JsonEditor.qml + SettingsPopup.qml + ManifestTable.qml + NodeHeader.qml + StatusWidgets.qml + DebugPanel.qml ) # Set up QML module directory for runtime diff --git a/src/qml/DeleteIcon.qml b/src/qml/DeleteIcon.qml index 2886705..1db7cd4 100644 --- a/src/qml/DeleteIcon.qml +++ b/src/qml/DeleteIcon.qml @@ -1,12 +1,6 @@ import QtQuick -// X / delete icon +// qmllint disable unqualified 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 - ] + 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] } diff --git a/src/qml/DiskWidget.qml b/src/qml/DiskWidget.qml index a158b4e..be0eab7 100644 --- a/src/qml/DiskWidget.qml +++ b/src/qml/DiskWidget.qml @@ -3,66 +3,47 @@ import QtQuick.Layouts import Logos.Theme import Logos.Controls -Rectangle { +ArcWidget { id: root - width: 140; height: 140 - radius: 14 - color: Theme.palette.backgroundSecondary - border.color: Theme.palette.borderSecondary - border.width: 1 + property var backend: mockBackend + property double total: 0 + property double used: 0 - property real total: 0 - property real used: 0 + fraction: root.total > 0 ? Math.min(root.used / root.total, 1.0) : 0 function formatBytes(bytes) { - if (bytes <= 0) return "0 B" - if (bytes < 1024) return bytes + " B" - if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB" - if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB" + if (bytes <= 0) { + return "0 B" + } + + if (bytes < 1024) { + return bytes + " B" + } + + if (bytes < 1024 * 1024) { + return (bytes / 1024).toFixed(1) + " KB" + } + + if (bytes < 1024 * 1024 * 1024) { + return (bytes / (1024 * 1024)).toFixed(1) + " MB" + } + return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" } - onTotalChanged: arc.requestPaint() - onUsedChanged: arc.requestPaint() + function refreshSpace() { + let space = root.backend.space() + root.total = space.total + root.used = space.used + } - Canvas { - id: arc - anchors.fill: parent + Connections { + target: root.backend - Component.onCompleted: requestPaint() - - onPaint: { - var ctx = getContext("2d") - ctx.reset() - - var cx = width / 2 - var cy = height / 2 - var r = 46 - var lw = 8 - var startRad = 130 * Math.PI / 180 - var totalRad = 280 * Math.PI / 180 - - // ── Background track (available / grey) ─────────────────────────── - ctx.beginPath() - ctx.arc(cx, cy, r, startRad, startRad + totalRad) - ctx.strokeStyle = Theme.palette.textMuted.toString() - ctx.lineWidth = lw - ctx.lineCap = "round" - ctx.stroke() - - // ── Fill (used / white) ─────────────────────────────────────────── - if (root.total > 0) { - var fraction = Math.min(root.used / root.total, 1.0) - if (fraction > 0) { - ctx.beginPath() - ctx.arc(cx, cy, r, startRad, startRad + totalRad * fraction) - ctx.strokeStyle = Theme.palette.text.toString() - ctx.lineWidth = lw - ctx.lineCap = "round" - ctx.stroke() - } - } + function onSpaceUpdated(total, used) { + root.total = total + root.used = used } } @@ -85,4 +66,19 @@ Rectangle { Layout.alignment: Qt.AlignHCenter } } + + QtObject { + id: mockBackend + + signal spaceUpdated(double total, double used) + signal uploadCompleted + signal downloadCompleted + + function space() { + return { + "total": 0, + "used": 0 + } + } + } } diff --git a/src/qml/DotIcon.qml b/src/qml/DotIcon.qml index de2a32a..dc22a0c 100644 --- a/src/qml/DotIcon.qml +++ b/src/qml/DotIcon.qml @@ -1,8 +1,6 @@ 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 +// qmllint disable unqualified Item { id: root @@ -53,8 +51,8 @@ Item { opacity: { if (!root.animated) { - return (index < root.pattern.length && root.pattern[index]) - ? root.activeOpacity : root.inactiveOpacity + return (index < root.pattern.length + && root.pattern[index]) ? root.activeOpacity : root.inactiveOpacity } // Wave from center const cx = Math.floor(root.columns / 2) @@ -64,8 +62,10 @@ Item { 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 + if (diff === 0) + return root.activeOpacity + if (diff === 1) + return 0.35 return root.inactiveOpacity } } diff --git a/src/qml/HealthIndicator.qml b/src/qml/HealthIndicator.qml index 3f18b7c..bdb5184 100644 --- a/src/qml/HealthIndicator.qml +++ b/src/qml/HealthIndicator.qml @@ -1,31 +1,45 @@ import QtQuick -import Logos.Theme -Item { +QtObject { id: root + property var backend property bool nodeIsUp: false - property var backend: mockBackend + property bool blinkOn: true + readonly property int threeMinutes: 180000 + + // Backend status readonly property int running: 2 - Timer { - readonly property int threeMinutes: 180000 - - interval: threeMinutes + // 600 ms blink toggle + property Timer blinkTimer: Timer { + interval: 600 repeat: true - running: root.backend.status == root.running - triggeredOnStart: true - onTriggered: root.backend.checkNodeIsUp() + running: true + onTriggered: root.blinkOn = !root.blinkOn } - Connections { + // Reachability check every 3 minutes while running + property Timer checkTimer: Timer { + interval: root.threeMinutes + repeat: true + running: root.backend !== null && root.backend.status === root.running + triggeredOnStart: true + onTriggered: function () { + if (root.backend) { + root.backend.checkNodeIsUp() + } + } + } + + property Connections connections: Connections { target: root.backend function onNodeIsUp() { root.nodeIsUp = true } - function onNodeIsntUp(reason) { + function onNodeIsntUp(r) { root.nodeIsUp = false } @@ -35,45 +49,4 @@ Item { } } } - - 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) - } } diff --git a/src/qml/JsonEditor.qml b/src/qml/JsonEditor.qml index 59fa7b7..3946419 100644 --- a/src/qml/JsonEditor.qml +++ b/src/qml/JsonEditor.qml @@ -27,6 +27,10 @@ Rectangle { border.color: root.isValid ? Theme.palette.borderSecondary : Theme.palette.error border.width: 1 + function load(_text) { + text = _text + } + function validate() { try { JSON.parse(jsonArea.text) diff --git a/src/qml/LogosStorageButton.qml b/src/qml/LogosStorageButton.qml index 90fcddf..c8e919d 100644 --- a/src/qml/LogosStorageButton.qml +++ b/src/qml/LogosStorageButton.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import Logos.Theme +// qmllint disable unqualified Button { id: control padding: Theme.spacing.small @@ -27,18 +28,22 @@ Button { background: Rectangle { color: { - if (!control.enabled) + if (!control.enabled) { return Theme.palette.backgroundElevated - if (control.isSuccess) + } + if (control.isSuccess) { return Theme.palette.success + } return Theme.palette.backgroundSecondary } border.width: 1 border.color: { - if (!control.enabled) + if (!control.enabled) { return Theme.palette.border - if (control.isSuccess) + } + if (control.isSuccess) { return Theme.palette.success + } return Theme.palette.border } radius: Theme.spacing.tiny diff --git a/src/qml/LogosTextField.qml b/src/qml/LogosTextField.qml index 63cd9d5..08ad920 100644 --- a/src/qml/LogosTextField.qml +++ b/src/qml/LogosTextField.qml @@ -2,15 +2,19 @@ import QtQuick import QtQuick.Controls import Logos.Theme +// qmllint disable unqualified TextField { id: root property bool isValid: acceptableInput && text.length > 0 + height: 60 placeholderTextColor: Theme.palette.textPlaceholder color: isValid ? Theme.palette.text : Theme.palette.error selectByMouse: true background: Rectangle { + height: 60 + Rectangle { anchors.fill: parent color: Theme.palette.backgroundSecondary diff --git a/src/qml/Main.qml b/src/qml/Main.qml index a5c0a31..e4a95f6 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -28,18 +28,6 @@ Item { Connections { target: root.backend - // 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) { - - // // stackView.pop() - // } - // } - // When the onboarding is completed, // the user should have a config save in his // home folder. diff --git a/src/qml/ManifestTable.qml b/src/qml/ManifestTable.qml index e3ac830..02922bb 100644 --- a/src/qml/ManifestTable.qml +++ b/src/qml/ManifestTable.qml @@ -1,28 +1,56 @@ import QtQuick import QtQuick.Controls +import QtQuick.Dialogs import QtQuick.Layouts +import QtCore import Logos.Theme import Logos.Controls +// qmllint disable unqualified ColumnLayout { id: root property var backend property bool running: false - - signal downloadRequested(var manifest) + property var manifests: [] spacing: Theme.spacing.small + FileDialog { + id: saveDialog + + property var pendingManifest: null + + fileMode: FileDialog.SaveFile + onAccepted: { + if (pendingManifest) { + root.backend.tryDownloadFile(pendingManifest.cid, selectedFile) + pendingManifest = null + } + } + onRejected: pendingManifest = null + } + function formatBytes(bytes) { - if (bytes <= 0) return "0 B" - if (bytes < 1024) return bytes + " B" - if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB" - if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB" + if (bytes <= 0) + return "0 B" + if (bytes < 1024) + return bytes + " B" + if (bytes < 1024 * 1024) + return (bytes / 1024).toFixed(1) + " KB" + if (bytes < 1024 * 1024 * 1024) + return (bytes / (1024 * 1024)).toFixed(1) + " MB" return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" } - // ── Section title ───────────────────────────────────────────────────────── + Connections { + target: root.backend + + onManifestsUpdated: function (manifests) { + root.manifests = manifests + } + } + LogosText { text: "MANIFESTS" font.pixelSize: 11 @@ -30,7 +58,6 @@ ColumnLayout { font.letterSpacing: 1.5 } - // ── CID input + fetch button ────────────────────────────────────────────── RowLayout { Layout.fillWidth: true spacing: Theme.spacing.small @@ -38,11 +65,12 @@ ColumnLayout { LogosTextField { id: cidInput Layout.fillWidth: true - placeholderText: "Enter CID to fetch manifest…" + placeholderText: "Enter CID to download manifest…" + isValid: true } LogosStorageButton { - text: "↓ Fetch" + text: "GET MANIFEST" enabled: root.running && cidInput.text.length > 0 onClicked: { root.backend.downloadManifest(cidInput.text) @@ -51,10 +79,9 @@ ColumnLayout { } } - // ── Table header ────────────────────────────────────────────────────────── Rectangle { Layout.fillWidth: true - height: 30 + Layout.preferredHeight: 30 color: Theme.palette.backgroundElevated radius: 4 @@ -62,17 +89,48 @@ ColumnLayout { anchors.fill: parent anchors.leftMargin: 10 - Text { width: 160; text: "CID"; color: Theme.palette.textSecondary; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } - Text { width: 130; text: "Filename"; color: Theme.palette.textSecondary; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } - Text { width: 90; text: "MIME"; color: Theme.palette.textSecondary; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } - Text { width: 80; text: "Size"; color: Theme.palette.textSecondary; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } + Text { + width: 160 + text: "CID" + color: Theme.palette.textSecondary + font.pixelSize: 11 + font.bold: true + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 130 + text: "Filename" + color: Theme.palette.textSecondary + font.pixelSize: 11 + font.bold: true + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 90 + text: "MIME" + color: Theme.palette.textSecondary + font.pixelSize: 11 + font.bold: true + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 80 + text: "Size" + color: Theme.palette.textSecondary + font.pixelSize: 11 + font.bold: true + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } } } - // ── Table body ──────────────────────────────────────────────────────────── Rectangle { Layout.fillWidth: true - height: 240 + Layout.preferredHeight: 240 color: Theme.palette.background border.color: Theme.palette.borderSecondary border.width: 1 @@ -82,13 +140,15 @@ ColumnLayout { ListView { id: manifestList anchors.fill: parent - model: root.backend ? root.backend.manifests : [] + model: root.manifests clip: true delegate: Rectangle { + id: delegateItem width: manifestList.width height: 36 - color: index % 2 === 0 ? Theme.palette.background : Theme.palette.backgroundSecondary + color: index % 2 + === 0 ? Theme.palette.background : Theme.palette.backgroundSecondary Row { anchors.fill: parent @@ -97,19 +157,21 @@ ColumnLayout { Text { width: 160 - text: modelData["cid"] ?? "" + text: modelData.cid color: Theme.palette.text font.pixelSize: 11 font.family: "monospace" elide: Text.ElideMiddle anchors.verticalCenter: parent.verticalCenter ToolTip.visible: cidHover.hovered - ToolTip.text: modelData["cid"] ?? "" - HoverHandler { id: cidHover } + ToolTip.text: modelData.cid + HoverHandler { + id: cidHover + } } Text { width: 130 - text: modelData["filename"] ?? "" + text: modelData.filename color: Theme.palette.textSecondary font.pixelSize: 11 elide: Text.ElideRight @@ -117,7 +179,7 @@ ColumnLayout { } Text { width: 90 - text: modelData["mimetype"] ?? "" + text: modelData.mimetype color: Theme.palette.textSecondary font.pixelSize: 11 elide: Text.ElideRight @@ -125,82 +187,92 @@ ColumnLayout { } Text { width: 80 - text: root.formatBytes(parseInt(modelData["datasetSize"] ?? "0")) + text: root.formatBytes(parseInt(modelData.datasetSize)) color: Theme.palette.textSecondary font.pixelSize: 11 elide: Text.ElideRight anchors.verticalCenter: parent.verticalCenter } - // ── Action buttons ──────────────────────────────────────── Row { spacing: 6 anchors.verticalCenter: parent.verticalCenter - // Download Rectangle { - width: 28; height: 28; radius: 4 + width: 28 + height: 28 + radius: 4 color: dlHover.hovered ? Theme.palette.backgroundElevated : "transparent" border.color: Theme.palette.borderSecondary border.width: 1 opacity: root.running ? 1.0 : 0.35 - Text { + DownloadIcon { anchors.centerIn: parent - text: "↓" - color: Theme.palette.text - font.pixelSize: 14 + dotColor: Theme.palette.text + dotSize: 3 + dotSpacing: 1 + } + HoverHandler { + id: dlHover } - HoverHandler { id: dlHover } MouseArea { anchors.fill: parent enabled: root.running cursorShape: Qt.PointingHandCursor - onClicked: root.downloadRequested(modelData) + onClicked: { + saveDialog.pendingManifest = modelData + saveDialog.currentFile = StandardPaths.writableLocation( + StandardPaths.HomeLocation) + + "/" + (modelData.filename + || modelData.cid + || "download") + saveDialog.open() + } } } - // Delete Rectangle { - width: 28; height: 28; radius: 4 + width: 28 + height: 28 + radius: 4 color: rmHover.hovered ? Theme.palette.backgroundElevated : "transparent" border.color: Theme.palette.borderSecondary border.width: 1 opacity: root.running ? 1.0 : 0.35 - Text { + DeleteIcon { anchors.centerIn: parent - text: "×" - color: Theme.palette.error - font.pixelSize: 16 - font.bold: true + dotColor: Theme.palette.error + dotSize: 3 + dotSpacing: 1 + } + HoverHandler { + id: rmHover } - HoverHandler { id: rmHover } MouseArea { anchors.fill: parent enabled: root.running cursorShape: Qt.PointingHandCursor - onClicked: root.backend.remove(modelData["cid"] ?? "") + onClicked: { + if (modelData.cid.length > 0) { + root.backend.remove(modelData.cid) + } + } } } } } } - // ── Empty state ─────────────────────────────────────────────────── + // Empty state ColumnLayout { anchors.centerIn: parent spacing: 10 visible: manifestList.count === 0 DotIcon { - pattern: [ - 0, 0, 1, 0, 0, - 0, 1, 0, 1, 0, - 1, 0, 0, 0, 1, - 0, 1, 0, 1, 0, - 0, 0, 1, 0, 0 - ] + pattern: [0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0] dotColor: Theme.palette.textMuted activeOpacity: 0.25 Layout.alignment: Qt.AlignHCenter diff --git a/src/qml/ModeSelector.qml b/src/qml/ModeSelector.qml index eaac2c9..142c118 100644 --- a/src/qml/ModeSelector.qml +++ b/src/qml/ModeSelector.qml @@ -30,19 +30,21 @@ LogosStorageLayout { Layout.fillWidth: true } - Item { height: Theme.spacing.medium } + Item { + Layout.preferredHeight: 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.color: root.selectedMode + === 0 ? Theme.palette.text : Theme.palette.borderTertiaryMuted border.width: root.selectedMode === 0 ? 2 : 1 ColumnLayout { @@ -80,13 +82,13 @@ LogosStorageLayout { } } - // ── 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.color: root.selectedMode + === 1 ? Theme.palette.text : Theme.palette.borderTertiaryMuted border.width: root.selectedMode === 1 ? 2 : 1 ColumnLayout { @@ -125,7 +127,9 @@ LogosStorageLayout { } } - Item { height: Theme.spacing.small } + Item { + Layout.preferredHeight: Theme.spacing.small + } LogosStorageButton { text: "Continue" diff --git a/src/qml/NodeStatusIcon.qml b/src/qml/NodeStatusIcon.qml index 8c42999..4df6fab 100644 --- a/src/qml/NodeStatusIcon.qml +++ b/src/qml/NodeStatusIcon.qml @@ -1,11 +1,7 @@ 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) +// qmllint disable unqualified Item { id: root @@ -42,8 +38,10 @@ Item { radius: root.dotSize * 0.25 color: { - if (root.success) return Theme.palette.success - if (!root.starting) return Theme.palette.error + if (root.success) + return Theme.palette.success + if (!root.starting) + return Theme.palette.error return Theme.palette.text } @@ -55,12 +53,15 @@ Item { 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 + if (diff === 0) + return 0.9 + if (diff === 1) + return 0.35 return 0.1 } - if (root.success) return 0.85 + if (root.success) + return 0.85 // Error — X pattern return (col === row || col + row === 6) ? 0.9 : 0.1 diff --git a/src/qml/OnBoarding.qml b/src/qml/OnBoarding.qml index 4843c89..5a5dd60 100644 --- a/src/qml/OnBoarding.qml +++ b/src/qml/OnBoarding.qml @@ -33,19 +33,21 @@ LogosStorageLayout { Layout.fillWidth: true } - Item { height: Theme.spacing.medium } + Item { + Layout.preferredHeight: 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.color: root.selectedMode + === 0 ? Theme.palette.text : Theme.palette.borderTertiaryMuted border.width: root.selectedMode === 0 ? 2 : 1 ColumnLayout { @@ -83,13 +85,13 @@ LogosStorageLayout { } } - // ── 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.color: root.selectedMode + === 1 ? Theme.palette.text : Theme.palette.borderTertiaryMuted border.width: root.selectedMode === 1 ? 2 : 1 ColumnLayout { @@ -128,7 +130,9 @@ LogosStorageLayout { } } - Item { height: Theme.spacing.small } + Item { + Layout.preferredHeight: Theme.spacing.small + } RowLayout { Layout.alignment: Qt.AlignHCenter diff --git a/src/qml/PeersWidget.qml b/src/qml/PeersWidget.qml index 8213b6c..c2e1665 100644 --- a/src/qml/PeersWidget.qml +++ b/src/qml/PeersWidget.qml @@ -3,66 +3,21 @@ import QtQuick.Layouts import Logos.Theme import Logos.Controls -Rectangle { +ArcWidget { id: root - width: 140; height: 140 - radius: 14 - color: Theme.palette.backgroundSecondary - border.color: Theme.palette.borderSecondary - border.width: 1 - - property int peerCount: 0 - // Soft ceiling: arc is full at maxPeers connected peers + property var backend + property int peers: 0 property int maxPeers: 20 - onPeerCountChanged: arc.requestPaint() - - Canvas { - id: arc - anchors.fill: parent - - Component.onCompleted: requestPaint() - - onPaint: { - var ctx = getContext("2d") - ctx.reset() - - var cx = width / 2 - var cy = height / 2 - var r = 46 - var lw = 8 - var startRad = 130 * Math.PI / 180 - var totalRad = 280 * Math.PI / 180 - - // ── Background track ────────────────────────────────────────────── - ctx.beginPath() - ctx.arc(cx, cy, r, startRad, startRad + totalRad) - ctx.strokeStyle = Theme.palette.textMuted.toString() - ctx.lineWidth = lw - ctx.lineCap = "round" - ctx.stroke() - - // ── Fill (peers / white) ─────────────────────────────────────────── - var fraction = root.maxPeers > 0 - ? Math.min(root.peerCount / root.maxPeers, 1.0) : 0 - if (fraction > 0) { - ctx.beginPath() - ctx.arc(cx, cy, r, startRad, startRad + totalRad * fraction) - ctx.strokeStyle = Theme.palette.text.toString() - ctx.lineWidth = lw - ctx.lineCap = "round" - ctx.stroke() - } - } - } + fraction: root.maxPeers > 0 ? Math.min(root.peers / root.maxPeers, 1.0) : 0 ColumnLayout { anchors.centerIn: parent spacing: 2 LogosText { - text: root.peerCount + text: root.peers font.pixelSize: 22 font.bold: true Layout.alignment: Qt.AlignHCenter @@ -76,4 +31,12 @@ Rectangle { Layout.alignment: Qt.AlignHCenter } } + + Connections { + target: root.backend + + function onPeersUpdated(peers) { + root.peers = peers + } + } } diff --git a/src/qml/SettingsIcon.qml b/src/qml/SettingsIcon.qml index 5a735ec..e18d4ed 100644 --- a/src/qml/SettingsIcon.qml +++ b/src/qml/SettingsIcon.qml @@ -1,12 +1,11 @@ import QtQuick -// Settings / gear icon +// Gear / cog icon — 4 cardinal teeth + ring with center hole +// . . ●. . +// . ● ● ● . +// ● ● . ● ● +// . ● ● ● . +// . . ● . . DotIcon { - pattern: [ - 0, 1, 0, 1, 0, - 1, 1, 1, 1, 1, - 0, 1, 1, 1, 0, - 1, 1, 1, 1, 1, - 0, 1, 0, 1, 0 - ] + pattern: [0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0] } diff --git a/src/qml/SettingsPopup.qml b/src/qml/SettingsPopup.qml index a7b04c7..f359600 100644 --- a/src/qml/SettingsPopup.qml +++ b/src/qml/SettingsPopup.qml @@ -16,6 +16,9 @@ Popup { padding: 24 closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + // Reload the live config every time the popup opens + onOpened: jsonEditor.load(root.backend.configJson() || "{}") + background: Rectangle { color: Theme.palette.backgroundSecondary border.color: Theme.palette.borderSecondary @@ -43,54 +46,12 @@ Popup { Layout.fillWidth: true } - // ── JSON editor ─────────────────────────────────────────────────────── - Rectangle { + JsonEditor { + id: jsonEditor 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 - - function validate() { - try { JSON.parse(text); isValid = true } - catch (e) { isValid = false } - } - - onTextChanged: validate() - - Component.onCompleted: { - text = (root.backend && root.backend.configJson) - ? root.backend.configJson : "{}" - validate() - } - - Connections { - target: root.backend - function onConfigJsonChanged() { - jsonArea.text = root.backend.configJson - } - } - } - } } - // ── Buttons ─────────────────────────────────────────────────────────── RowLayout { Layout.alignment: Qt.AlignHCenter spacing: Theme.spacing.medium @@ -103,10 +64,10 @@ Popup { LogosStorageButton { text: "Save" variant: "success" - enabled: jsonArea.isValid + enabled: jsonEditor.isValid onClicked: { - root.backend.saveUserConfig(jsonArea.text) - root.backend.reloadIfChanged(jsonArea.text) + root.backend.saveUserConfig(jsonEditor.text) + root.backend.reloadIfChanged(jsonEditor.text) root.close() } } diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index 3ee0cd7..eeb985c 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -1,8 +1,6 @@ import QtQuick import QtQuick.Controls -import QtQuick.Dialogs import QtQuick.Layouts -import QtCore import Logos.Theme import Logos.Controls @@ -11,34 +9,10 @@ LogosStorageLayout { id: root property var backend: mockBackend - readonly property int stopped: 0 - readonly property int starting: 1 - readonly property int running: 2 - readonly property int stopping: 3 - readonly property int destroyed: 4 - property int peerCount: 0 - property var pendingDownloadManifest: null property bool showDebug: false function isRunning() { - return backend.status === running - } - - function getStatusLabel() { - switch (backend.status) { - case stopped: - return "Stopped" - case starting: - return "Starting…" - case running: - return "Running" - case stopping: - return "Stopping…" - case destroyed: - return "Not initialised" - default: - return "" - } + return backend.status === 2 // StorageBackend.Running } Component.onCompleted: root.backend.start() @@ -48,53 +22,26 @@ LogosStorageLayout { backend: root.backend } - Connections { - target: root.backend - function onPeersUpdated(count) { - root._peerCount = count - } - } - - // ── Clipboard helper (Qt6 has no Qt.copyToClipboard) ───────────────────── - TextEdit { - id: clipHelper - visible: false - function copyText(str) { - clipHelper.text = str - clipHelper.selectAll() - clipHelper.copy() - } - } - - // ── Mock backend ────────────────────────────────────────────────────────── QtObject { id: mockBackend - property var status: root.stopped + property var status: 0 property var debugLogs: "Hello!" - property var configJson: "{}" property string uploadStatus: "" property int uploadProgress: 0 property var manifests: [] - property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 - property var quotaUsedBytes: 0 - property string cid: "" - signal nodeIsUp signal nodeIsntUp(string reason) signal peersUpdated(int count) + signal uploadCompleted(string cid) + signal downloadCompleted(string cid) - function start() { - status = root.running - } - function stop() { - status = root.stopped - } + function start() { status = 2 } + function stop() { status = 0 } function checkNodeIsUp() {} function tryUploadFile(f) {} function downloadManifest(c) {} function remove(c) {} function tryDownloadFile(c, d) {} - function space() {} function tryDebug() {} function showPeerId() {} function dataDir() {} @@ -102,47 +49,19 @@ LogosStorageLayout { function version() {} function saveUserConfig(j) {} function reloadIfChanged(j) {} - function configJson() { - return "{}" - } - function peerCount() { - return 0 - } + function configJson() { return "{}" } } - // ── File dialogs ────────────────────────────────────────────────────────── - FileDialog { - id: fileDialog - onAccepted: root.backend.tryUploadFile(fileDialog.selectedFile) - } - - FileDialog { - id: manifestSaveDialog - fileMode: FileDialog.SaveFile - onAccepted: { - if (root.pendingDownloadManifest) { - root.backend.tryDownloadFile( - root.pendingDownloadManifest["cid"], - manifestSaveDialog.selectedFile) - root.pendingDownloadManifest = null - } - } - onRejected: root.pendingDownloadManifest = null - } - - // ── Settings popup ──────────────────────────────────────────────────────── SettingsPopup { id: settingsPopup backend: root.backend } - // ── Ctrl+D toggle ───────────────────────────────────────────────────────── Shortcut { sequence: "Ctrl+D" onActivated: root.showDebug = !root.showDebug } - // ── Main scrollable content ─────────────────────────────────────────────── ScrollView { id: mainScroll anchors.fill: parent @@ -154,132 +73,16 @@ LogosStorageLayout { width: mainScroll.availableWidth spacing: 0 - // ══════════════════════════════════════════════════════════════════ - // Header — node identity + settings + start/stop - // ══════════════════════════════════════════════════════════════════ - RowLayout { + NodeHeader { Layout.fillWidth: true Layout.leftMargin: 24 Layout.rightMargin: 24 Layout.topMargin: 24 Layout.bottomMargin: 20 - spacing: Theme.spacing.medium - - StorageIcon { - animated: root.backend.status === root.starting - || root.backend.status === root.stopping - dotColor: { - if (root.backend.status === root.starting) - return Theme.palette.warning - if (!root.isRunning()) - return Theme.palette.textMuted - return health.nodeIsUp ? Theme.palette.success : Theme.palette.error - } - } - - ColumnLayout { - spacing: 6 - - LogosText { - text: "Logos Storage" - font.pixelSize: Theme.typography.titleText - } - - RowLayout { - spacing: 7 - - Rectangle { - Layout.preferredWidth: 7 - Layout.preferredHeight: 7 - radius: 3.5 - Layout.alignment: Qt.AlignVCenter - color: { - if (root.backend.status === root.starting) - return Theme.palette.warning - if (!root.isRunning()) - return Theme.palette.textMuted - return health.nodeIsUp ? Theme.palette.success : Theme.palette.error - } - opacity: root.isRunning( - ) ? (health.blinkOn ? 1.0 : 0.15) : 1.0 - } - - LogosText { - text: root.getStatusLabel() - font.pixelSize: Theme.typography.primaryText - color: Theme.palette.textSecondary - Layout.alignment: Qt.AlignVCenter - } - } - } - - Item { - Layout.fillWidth: true - } - - Rectangle { - Layout.preferredWidth: 44 - Layout.preferredHeight: 44 - radius: 8 - color: settingsHover.hovered ? Theme.palette.backgroundElevated : "transparent" - border.color: Theme.palette.borderSecondary - border.width: 1 - - SettingsIcon { - anchors.centerIn: parent - dotColor: Theme.palette.text - dotSize: 5 - dotSpacing: 2 - } - - HoverHandler { - id: settingsHover - } - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: settingsPopup.open() - } - } - - Rectangle { - Layout.preferredWidth: 44 - Layout.preferredHeight: 44 - radius: 8 - color: startStopHover.hovered ? Theme.palette.backgroundElevated : "transparent" - border.color: Theme.palette.borderSecondary - border.width: 1 - opacity: (root.backend.status === root.running - || root.backend.status === root.stopped) ? 1.0 : 0.4 - - PlayIcon { - anchors.centerIn: parent - dotColor: Theme.palette.text - dotSize: 5 - dotSpacing: 2 - visible: root.backend.status !== root.running - } - StopIcon { - anchors.centerIn: parent - dotColor: Theme.palette.text - dotSize: 5 - dotSpacing: 2 - visible: root.backend.status === root.running - } - - HoverHandler { - id: startStopHover - } - MouseArea { - anchors.fill: parent - enabled: root.backend.status === root.running - || root.backend.status === root.stopped - cursorShape: Qt.PointingHandCursor - onClicked: root.backend.status - === root.running ? root.backend.stop( - ) : root.backend.start() - } - } + backend: root.backend + nodeIsUp: health.nodeIsUp + blinkOn: health.blinkOn + onSettingsRequested: settingsPopup.open() } Rectangle { @@ -290,133 +93,13 @@ LogosStorageLayout { color: Theme.palette.borderSecondary } - RowLayout { + StatusWidgets { Layout.fillWidth: true Layout.leftMargin: 24 Layout.rightMargin: 24 Layout.topMargin: 20 - Layout.bottomMargin: 10 - spacing: Theme.spacing.medium - - UploadWidget { - uploadProgress: root.backend.uploadProgress - running: root.isRunning() - onUploadRequested: fileDialog.open() - } - - DiskWidget { - total: root.backend.quotaMaxBytes - used: root.backend.quotaUsedBytes - } - - PeersWidget { - peerCount: root.peerCount - } - - Item { - Layout.fillWidth: true - } - } - - Item { - Layout.fillWidth: true - Layout.leftMargin: 24 - Layout.rightMargin: 24 - Layout.bottomMargin: 20 - Layout.preferredHeight: 36 - - opacity: String(root.backend.cid).length > 0 ? 1.0 : 0.0 - - Behavior on opacity { - NumberAnimation { - duration: 200 - } - } - - Rectangle { - id: cidBadge - height: 36 - width: cidBadgeRow.implicitWidth + 28 - radius: 6 - color: Theme.palette.backgroundSecondary - border.color: Theme.palette.borderSecondary - border.width: 1 - - RowLayout { - id: cidBadgeRow - anchors.centerIn: parent - spacing: 8 - - LogosText { - text: "CID" - font.pixelSize: 10 - color: Theme.palette.textTertiary - } - - LogosText { - text: { - var c = String(root.backend.cid) - return c.length > 20 ? c.substring( - 0, - 8) + "…" + c.slice( - -6) : c - } - font.pixelSize: 11 - font.family: "monospace" - color: Theme.palette.text - } - - LogosText { - text: "COPY" - font.pixelSize: 9 - color: Theme.palette.textTertiary - font.letterSpacing: 0.8 - } - } - - // ── Green flash on copy ─────────────────────────────────── - Rectangle { - id: copyFlash - anchors.fill: parent - radius: parent.radius - color: Theme.palette.success - opacity: 0 - - SequentialAnimation on opacity { - id: copyFlashAnim - running: false - NumberAnimation { - to: 0.18 - duration: 80 - } - NumberAnimation { - to: 0 - duration: 500 - } - } - } - - HoverHandler { - id: cidBadgeHover - } - - Rectangle { - anchors.fill: parent - radius: parent.radius - color: cidBadgeHover.hovered ? Qt.rgba( - 1, 1, 1, - 0.04) : "transparent" - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - clipHelper.copyText(String(root.backend.cid)) - copyFlashAnim.restart() - } - } - } + backend: root.backend + running: root.isRunning() } Rectangle { @@ -435,14 +118,6 @@ LogosStorageLayout { Layout.bottomMargin: 20 backend: root.backend running: root.isRunning() - onDownloadRequested: function (manifest) { - root.pendingDownloadManifest = manifest - var filename = manifest["filename"] || manifest["cid"] - || "download" - manifestSaveDialog.currentFile = StandardPaths.writableLocation( - StandardPaths.HomeLocation) + "/" + filename - manifestSaveDialog.open() - } } Item { @@ -451,97 +126,14 @@ LogosStorageLayout { } } - Rectangle { + DebugPanel { id: debugPanel anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: 220 - color: Theme.palette.backgroundElevated - border.color: Theme.palette.borderSecondary - border.width: 1 visible: root.showDebug - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - // Dev action buttons - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 10 - Layout.topMargin: 6 - Layout.bottomMargin: 4 - spacing: 6 - - LogosStorageButton { - text: "Space" - enabled: root.isRunning() - onClicked: root.backend.space() - } - LogosStorageButton { - text: "Debug" - enabled: root.isRunning() - onClicked: root.backend.tryDebug() - } - LogosStorageButton { - text: "Peer ID" - enabled: root.isRunning() - onClicked: root.backend.showPeerId() - } - LogosStorageButton { - text: "Data dir" - enabled: root.isRunning() - onClicked: root.backend.dataDir() - } - LogosStorageButton { - text: "SPR" - enabled: root.isRunning() - onClicked: root.backend.spr() - } - LogosStorageButton { - text: "Version" - enabled: root.isRunning() - onClicked: root.backend.version() - } - Item { - Layout.fillWidth: true - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 - color: Theme.palette.borderSecondary - } - - // Logs - Flickable { - id: logFlick - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - contentWidth: width - contentHeight: debugText.paintedHeight - - TextEdit { - id: debugText - width: logFlick.width - text: root.backend.debugLogs - color: Theme.palette.textSecondary - font.family: "monospace" - font.pixelSize: 11 - wrapMode: Text.WrapAnywhere - readOnly: true - padding: 8 - bottomPadding: 20 - - onTextChanged: Qt.callLater(function () { - logFlick.contentY = Math.max( - 0, logFlick.contentHeight - logFlick.height) - }) - } - } - } + backend: root.backend + running: root.isRunning() } } diff --git a/src/qml/UploadWidget.qml b/src/qml/UploadWidget.qml index 44a806c..3df4ea3 100644 --- a/src/qml/UploadWidget.qml +++ b/src/qml/UploadWidget.qml @@ -6,15 +6,16 @@ import Logos.Controls ArcWidget { id: root - property int uploadProgress: 0 // 0–100 + property int uploadProgress: 0 // 0–100 property bool running: false - readonly property bool isUploading: uploadProgress > 0 && uploadProgress < 100 - readonly property bool isDone: uploadProgress >= 100 + readonly property bool isUploading: uploadProgress > 0 + && uploadProgress < 100 + readonly property bool isDone: uploadProgress >= 100 signal uploadRequested - fraction: root.uploadProgress / 100.0 + fraction: root.uploadProgress / 100.0 fillColor: root.isDone ? Theme.palette.success : Theme.palette.text // ── Center content ──────────────────────────────────────────────────────── @@ -24,11 +25,11 @@ ArcWidget { // Idle or done: upload icon UploadIcon { - dotColor: Theme.palette.textSecondary - dotSize: 4 - dotSpacing: 3 + dotColor: Theme.palette.textSecondary + dotSize: 4 + dotSpacing: 3 activeOpacity: 0.5 - visible: !root.isUploading + visible: !root.isUploading Layout.alignment: Qt.AlignHCenter } @@ -50,19 +51,22 @@ ArcWidget { } } - // ── Hover overlay ───────────────────────────────────────────────────────── - HoverHandler { id: widgetHover } + HoverHandler { + id: widgetHover + } Rectangle { anchors.fill: parent radius: root.radius - color: widgetHover.hovered && root.running ? Qt.rgba(1, 1, 1, 0.04) : "transparent" + color: widgetHover.hovered + && root.running ? Qt.rgba(1, 1, 1, 0.04) : "transparent" } - // ── Click → trigger upload when node is running ─────────────────────────── MouseArea { anchors.fill: parent cursorShape: root.running ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: if (root.running) root.uploadRequested() + onClicked: if (root.running) { + root.uploadRequested() + } } } diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index d1fe4c4..43a6ab7 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -13,13 +13,14 @@ qml/ModeSelector.qml qml/AdvancedSetup.qml qml/ManifestTable.qml + qml/NodeHeader.qml + qml/StatusWidgets.qml + qml/DebugPanel.qml qml/SettingsPopup.qml qml/JsonEditor.qml - qml/SpaceBar.qml qml/DiskWidget.qml qml/UploadWidget.qml qml/PeersWidget.qml - qml/icons/DotIcon.qml qml/icons/NodeStatusIcon.qml qml/icons/GuideIcon.qml @@ -34,7 +35,6 @@ qml/icons/DownloadIcon.qml qml/icons/DeleteIcon.qml qml/icons/ArcWidget.qml - icons/storage.png diff --git a/vendor/logos-design-system b/vendor/logos-design-system index 596811c..063c4b4 160000 --- a/vendor/logos-design-system +++ b/vendor/logos-design-system @@ -1 +1 @@ -Subproject commit 596811cbb0a0644322267368e87fab80e34203d8 +Subproject commit 063c4b46accc621bc85fa8baab46b31ef65f3957