From e178abae42fa6bec50ceb3d42d81442957a54b4e Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 17 Feb 2026 11:53:47 +0400 Subject: [PATCH 1/3] Add Manifest table --- src/StorageBackend.cpp | 60 ++++++++++++----- src/StorageBackend.h | 4 ++ src/qml/StorageView.qml | 144 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 188 insertions(+), 20 deletions(-) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index 2362797..4eec6d3 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -66,6 +66,7 @@ LogosResult StorageBackend::init(const QString& configJson = "{}") { } else { setStatus(Running); debug("Storage module started."); + QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); emit startCompleted(); } })) { @@ -631,18 +632,38 @@ void StorageBackend::downloadManifest(const QString& cid) { return; } - debug("Manifest tree cid: " + result.getString("treeCid")); - debug(QString("Manifest datasetSize %1").arg(result.getInt("datasetSize"))); - debug(QString("Manifest blockSize %1").arg(result.getInt("blockSize"))); - debug("Manifest filename: " + result.getString("filename")); - debug("Manifest mimetype: " + result.getString("mimetype")); + QString treeCid = result.getString("treeCid"); + qint64 datasetSize = result.getInt("datasetSize"); + qint64 blockSize = result.getInt("blockSize"); + QString filename = result.getString("filename"); + QString mimetype = result.getString("mimetype"); + + debug("Manifest tree cid: " + treeCid); + debug(QString("Manifest datasetSize %1").arg(datasetSize)); + debug(QString("Manifest blockSize %1").arg(blockSize)); + 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["datasetSize"] = datasetSize; + manifest["blockSize"] = blockSize; + + m_manifests.append(manifest); + emit manifestsChanged(); } +QVariantList StorageBackend::manifests() const { return m_manifests; } + void StorageBackend::downloadManifests() { qDebug() << "StorageBackend::downloadManifests called"; LogosResult result = m_logos->storage_module.manifests(); - QString error = result.getError(); + if (!result.success) { debug("StorageBackend::downloadManifests failed with error=" + result.getError()); return; @@ -650,22 +671,25 @@ void StorageBackend::downloadManifests() { QVariantList manifestsList = result.getList(); int count = manifestsList.size(); - debug(QString("Found %1 manifests").arg(count)); - // for (const QVariant& manifestVariant : manifestsList) { - // QVariantMap manifest = manifestVariant.toMap(); + m_manifests.clear(); - // QString cid = manifest["cid"].toString(); - // QString treeCid = manifest["treeCid"].toString(); - // QString filename = manifest["filename"].toString(); - // qint64 datasetSize = manifest["datasetSize"].toLongLong(); + for (const QVariant& manifestVariant : manifestsList) { + QVariantMap src = manifestVariant.toMap(); - // debug(QString("Manifest: %1, treeCid: %2, size: %3") - // .arg(filename) - // .arg(treeCid.isEmpty() ? "EMPTY" : treeCid) - // .arg(datasetSize)); - // } + 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(); } void StorageBackend::space() { diff --git a/src/StorageBackend.h b/src/StorageBackend.h index a920854..79fb1ce 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -33,6 +33,7 @@ class StorageBackend : public QObject { Q_PROPERTY(QString configJson READ configJson NOTIFY configJsonChanged) Q_PROPERTY(int uploadProgress READ uploadProgress NOTIFY uploadProgressChanged) Q_PROPERTY(QString uploadStatus READ uploadStatus NOTIFY uploadStatusChanged) + Q_PROPERTY(QVariantList manifests READ manifests NOTIFY manifestsChanged) public: enum StorageStatus { Stopped = 0, Starting, Running, Stopping, Destroyed }; @@ -44,6 +45,7 @@ class StorageBackend : public QObject { QString configJson() const; int uploadProgress() const; QString uploadStatus() const; + QVariantList manifests() const; Q_INVOKABLE static QString defaultDataDir(); @@ -88,6 +90,7 @@ class StorageBackend : public QObject { void configJsonChanged(); void uploadProgressChanged(); void uploadStatusChanged(); + void manifestsChanged(); private slots: @@ -106,4 +109,5 @@ class StorageBackend : public QObject { QString m_uploadStatus = ""; qint64 m_uploadTotalBytes = 0; qint64 m_uploadedBytes = 0; + QVariantList m_manifests; }; diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index 1b95f16..181683e 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -124,6 +124,8 @@ Rectangle { function space() {} function updateLogLevel(logLevel) {} + + property var manifests: [] } Text { @@ -392,6 +394,144 @@ Rectangle { anchors.topMargin: 10 } + // ── Manifests section ────────────────────────────────────────────────── + Text { + id: manifestsTitle + text: "Manifests" + color: "white" + font.pixelSize: 14 + font.bold: true + anchors.top: downloadManifestsButton.bottom + anchors.topMargin: 30 + anchors.horizontalCenter: parent.horizontalCenter + } + + Row { + id: manifestInputRow + spacing: 8 + anchors.top: manifestsTitle.bottom + anchors.topMargin: 8 + anchors.horizontalCenter: parent.horizontalCenter + + TextField { + id: manifestCidField + width: 380 + placeholderText: "Enter CID to download manifest" + placeholderTextColor: "#999999" + color: "#000000" + selectByMouse: true + } + + Button { + id: addManifestButton + text: "Download Manifest" + enabled: root.isRunning() && manifestCidField.text.length > 0 + onClicked: { + root.backend.downloadManifest(manifestCidField.text) + manifestCidField.clear() + } + } + } + + // Table header + Rectangle { + id: manifestTableHeader + anchors.top: manifestInputRow.bottom + anchors.topMargin: 8 + anchors.left: manifestInputRow.left + anchors.right: manifestInputRow.right + height: 28 + color: "#222222" + radius: 2 + + Row { + anchors.fill: parent + anchors.leftMargin: 6 + + Text { width: 200; text: "CID"; color: "#aaaaaa"; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } + Text { width: 140; text: "Filename"; color: "#aaaaaa"; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } + Text { width: 100; text: "MIME type"; color: "#aaaaaa"; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } + Text { width: 90; text: "Size (bytes)"; color: "#aaaaaa"; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } + } + } + + Rectangle { + id: manifestTableContainer + anchors.top: manifestTableHeader.bottom + anchors.left: manifestTableHeader.left + anchors.right: manifestTableHeader.right + height: 180 + color: "#111111" + border.color: "#333333" + border.width: 1 + clip: true + + ListView { + id: manifestListView + anchors.fill: parent + model: root.backend.manifests + clip: true + + delegate: Rectangle { + width: manifestListView.width + height: 28 + color: index % 2 === 0 ? "#181818" : "#1e1e1e" + + Row { + anchors.fill: parent + anchors.leftMargin: 6 + spacing: 0 + + Text { + width: 200 + text: modelData["cid"] ?? "" + color: "#dddddd" + font.pixelSize: 11 + font.family: "monospace" + elide: Text.ElideMiddle + anchors.verticalCenter: parent.verticalCenter + ToolTip.visible: hovered + ToolTip.text: modelData["cid"] ?? "" + HoverHandler {} + } + Text { + width: 140 + text: modelData["filename"] ?? "" + color: "#dddddd" + font.pixelSize: 11 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 100 + text: modelData["mimetype"] ?? "" + color: "#dddddd" + font.pixelSize: 11 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 90 + text: modelData["datasetSize"] ?? "" + color: "#dddddd" + font.pixelSize: 11 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Text { + anchors.centerIn: parent + text: "No manifests yet" + color: "#555555" + font.pixelSize: 12 + visible: manifestListView.count === 0 + } + } + } + + // ── Log level section ────────────────────────────────────────────────── TextField { id: logLevelField placeholderTextColor: "#999999" @@ -399,8 +539,8 @@ Rectangle { color: "black" // text: root.downloadCid onTextChanged: root.logLevel = text - anchors.top: downloadManifestsButton.bottom - anchors.topMargin: 50 + anchors.top: manifestTableContainer.bottom + anchors.topMargin: 30 anchors.horizontalCenter: parent.horizontalCenter } From ca52cf1e81ed1963aaba5147dc4448b955dd2e1b Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 17 Feb 2026 12:19:41 +0400 Subject: [PATCH 2/3] Clean the interface a bit --- src/StorageBackend.cpp | 9 + src/qml/StorageView.qml | 377 ++++++++++++++++++++++++---------------- 2 files changed, 241 insertions(+), 145 deletions(-) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index 4eec6d3..bbdef1a 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -555,6 +555,15 @@ void StorageBackend::remove(const QString& cid) { } debug("Cid " + cid + " removed."); + + // 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; + } + } } void StorageBackend::fetch(const QString& cid) { diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index 181683e..c256c7f 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -2,13 +2,14 @@ import QtQuick import QtQuick.Controls import QtQuick.Dialogs import QtQuick.Layouts +import QtCore Rectangle { id: root Layout.fillWidth: true Layout.fillHeight: true implicitWidth: 600 - implicitHeight: 400 + implicitHeight: 600 color: "#000000" property var backend: mockBackend @@ -22,6 +23,7 @@ Rectangle { property url downloadCid: "" property string logLevel: "" property bool showDebug: false + property var pendingDownloadManifest: null property url uploadCid: root.backend.cid property url configJson: root.backend.configJson @@ -215,36 +217,35 @@ Rectangle { } } - TextField { - id: peerIdField - placeholderText: "Enter the peer Id" - placeholderTextColor: "#999999" - color: "#000000" - selectByMouse: true - text: root.peerId - onTextChanged: root.peerId = text - anchors.top: uploadProgressColumn.bottom - anchors.topMargin: 50 - anchors.horizontalCenter: parent.horizontalCenter - } - - Button { - id: peerConnectButton - objectName: "peerConnectButton" - text: "Peer connect" - onClicked: root.backend.tryPeerConnect(root.peerId) - anchors.top: peerIdField.bottom - anchors.horizontalCenter: parent.horizontalCenter - enabled: root.isRunning - anchors.topMargin: 10 - } + // TextField { + // id: peerIdField + // placeholderText: "Enter the peer Id" + // placeholderTextColor: "#999999" + // color: "#000000" + // selectByMouse: true + // text: root.peerId + // onTextChanged: root.peerId = text + // anchors.top: uploadProgressColumn.bottom + // anchors.topMargin: 50 + // anchors.horizontalCenter: parent.horizontalCenter + // } + // Button { + // id: peerConnectButton + // objectName: "peerConnectButton" + // text: "Peer connect" + // onClicked: root.backend.tryPeerConnect(root.peerId) + // anchors.top: peerIdField.bottom + // anchors.horizontalCenter: parent.horizontalCenter + // enabled: root.isRunning + // anchors.topMargin: 10 + // } Button { id: debugButton objectName: "debugButton" text: "Debug" onClicked: root.backend.tryDebug() - anchors.top: peerConnectButton.bottom + anchors.top: uploadProgressColumn.bottom anchors.horizontalCenter: parent.horizontalCenter enabled: root.isRunning anchors.topMargin: 50 @@ -255,7 +256,7 @@ Rectangle { objectName: "peerIdButton" text: "Peer Id" onClicked: root.backend.showPeerId() - anchors.top: peerConnectButton.bottom + anchors.top: uploadProgressColumn.bottom anchors.right: debugButton.left enabled: root.isRunning anchors.topMargin: 50 @@ -266,7 +267,7 @@ Rectangle { objectName: "dataDirButton" text: "Data dir" onClicked: root.backend.dataDir() - anchors.top: peerConnectButton.bottom + anchors.top: uploadProgressColumn.bottom anchors.right: peerIdButton.left enabled: root.isRunning anchors.topMargin: 50 @@ -277,7 +278,7 @@ Rectangle { objectName: "sprButton" text: "SPR" onClicked: root.backend.spr() - anchors.top: peerConnectButton.bottom + anchors.top: uploadProgressColumn.bottom anchors.left: debugButton.right enabled: root.isRunning anchors.topMargin: 50 @@ -288,91 +289,92 @@ Rectangle { objectName: "versionButton" text: "Version" onClicked: root.backend.version() - anchors.top: peerConnectButton.bottom + anchors.top: uploadProgressColumn.bottom anchors.left: sprButton.right enabled: root.isRunning anchors.topMargin: 50 } - TextField { - id: cidDownloadField - placeholderTextColor: "#999999" - placeholderText: "Enter the cid to download" - color: "black" - // text: root.downloadCid - onTextChanged: root.downloadCid = text - anchors.top: debugButton.bottom - anchors.topMargin: 50 - anchors.horizontalCenter: parent.horizontalCenter - } + // TextField { + // id: cidDownloadField + // placeholderTextColor: "#999999" + // placeholderText: "Enter the cid to download" + // color: "black" + // // text: root.downloadCid + // onTextChanged: root.downloadCid = text + // anchors.top: debugButton.bottom + // anchors.topMargin: 50 + // anchors.horizontalCenter: parent.horizontalCenter + // } - Button { - id: openFile2 - text: "Open file" - onClicked: fileDialog2.open() - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: cidDownloadField.bottom - anchors.topMargin: 15 - enabled: root.isRunning - } + // Button { + // id: openFile2 + // text: "Open file" + // onClicked: fileDialog2.open() + // anchors.horizontalCenter: parent.horizontalCenter + // anchors.top: cidDownloadField.bottom + // anchors.topMargin: 15 + // enabled: root.isRunning + // } - Button { - id: cidDownloadButton - objectName: "cidDownloadButton" - text: "Download" - onClicked: root.backend.tryDownloadFile(root.downloadCid, - root.downloadDestination) - anchors.top: openFile2.bottom - anchors.horizontalCenter: parent.horizontalCenter - enabled: root.isRunning - anchors.topMargin: 10 - } + // Button { + // id: cidDownloadButton + // objectName: "cidDownloadButton" + // text: "Download" + // onClicked: root.backend.tryDownloadFile(root.downloadCid, + // root.downloadDestination) + // anchors.top: openFile2.bottom + // anchors.horizontalCenter: parent.horizontalCenter + // enabled: root.isRunning + // anchors.topMargin: 10 + // } - Button { - id: existsButton - objectName: "existsButton" - text: "Exists" - onClicked: root.backend.exists(root.downloadCid) - anchors.top: openFile2.bottom - anchors.left: cidDownloadButton.right - enabled: root.isRunning - anchors.topMargin: 10 - } + // Button { + // id: existsButton + // objectName: "existsButton" + // text: "Exists" + // onClicked: root.backend.exists(root.downloadCid) + // anchors.top: openFile2.bottom + // anchors.left: cidDownloadButton.right + // enabled: root.isRunning + // anchors.topMargin: 10 + // } - Button { - id: fetchButton - objectName: "fetchButton" - text: "Fetch" - onClicked: root.backend.fetch(root.downloadCid) - anchors.top: openFile2.bottom - anchors.left: existsButton.right - enabled: root.isRunning - anchors.topMargin: 10 - } + // Button { + // id: fetchButton + // objectName: "fetchButton" + // text: "Fetch" + // onClicked: root.backend.fetch(root.downloadCid) + // anchors.top: openFile2.bottom + // anchors.left: existsButton.right + // enabled: root.isRunning + // anchors.topMargin: 10 + // } - Button { - id: removeButton - objectName: "removeButton" - text: "Remove" - onClicked: root.backend.remove(root.downloadCid) - anchors.top: openFile2.bottom - anchors.right: cidDownloadButton.left - enabled: root.isRunning - anchors.topMargin: 10 - } + // Button { + // id: removeButton + // objectName: "removeButton" + // text: "Remove" + // onClicked: root.backend.remove(root.downloadCid) + // anchors.top: openFile2.bottom + // anchors.right: cidDownloadButton.left + // enabled: root.isRunning + // anchors.topMargin: 10 + // } - Button { - id: downloadManifestButton - objectName: "downloadManifestButton" - text: "Download manifest" - onClicked: root.backend.downloadManifest(root.downloadCid) - anchors.top: openFile2.bottom - anchors.right: removeButton.left - enabled: root.isRunning - anchors.topMargin: 10 - } + // Button { + // id: downloadManifestButton + // objectName: "downloadManifestButton" + // text: "Download manifest" + // onClicked: root.backend.downloadManifest(root.downloadCid) + // anchors.top: openFile2.bottom + // anchors.right: removeButton.left + // enabled: root.isRunning + // anchors.topMargin: 10 + // } - Button { + + /*Button { id: downloadManifestsButton objectName: "downloadManifestsButton" text: "Manifests" @@ -381,18 +383,7 @@ Rectangle { anchors.horizontalCenter: parent.horizontalCenter enabled: root.isRunning anchors.topMargin: 10 - } - - Button { - id: spaceButton - objectName: "spaceButton" - text: "Space" - onClicked: root.backend.space() - anchors.top: cidDownloadButton.bottom - anchors.right: downloadManifestsButton.left - enabled: root.isRunning - anchors.topMargin: 10 - } + }*/ // ── Manifests section ────────────────────────────────────────────────── Text { @@ -401,7 +392,7 @@ Rectangle { color: "white" font.pixelSize: 14 font.bold: true - anchors.top: downloadManifestsButton.bottom + anchors.top: versionButton.bottom anchors.topMargin: 30 anchors.horizontalCenter: parent.horizontalCenter } @@ -438,8 +429,8 @@ Rectangle { id: manifestTableHeader anchors.top: manifestInputRow.bottom anchors.topMargin: 8 - anchors.left: manifestInputRow.left - anchors.right: manifestInputRow.right + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 40 height: 28 color: "#222222" radius: 2 @@ -448,10 +439,50 @@ Rectangle { anchors.fill: parent anchors.leftMargin: 6 - Text { width: 200; text: "CID"; color: "#aaaaaa"; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } - Text { width: 140; text: "Filename"; color: "#aaaaaa"; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } - Text { width: 100; text: "MIME type"; color: "#aaaaaa"; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } - Text { width: 90; text: "Size (bytes)"; color: "#aaaaaa"; font.pixelSize: 11; font.bold: true; elide: Text.ElideRight; anchors.verticalCenter: parent.verticalCenter } + Text { + width: 150 + text: "CID" + color: "#aaaaaa" + font.pixelSize: 11 + font.bold: true + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 120 + text: "Filename" + color: "#aaaaaa" + font.pixelSize: 11 + font.bold: true + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 85 + text: "MIME type" + color: "#aaaaaa" + font.pixelSize: 11 + font.bold: true + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 75 + text: "Size (bytes)" + color: "#aaaaaa" + font.pixelSize: 11 + font.bold: true + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 110 + text: "" + color: "#aaaaaa" + font.pixelSize: 11 + font.bold: true + anchors.verticalCenter: parent.verticalCenter + } } } @@ -460,7 +491,7 @@ Rectangle { anchors.top: manifestTableHeader.bottom anchors.left: manifestTableHeader.left anchors.right: manifestTableHeader.right - height: 180 + height: 280 color: "#111111" border.color: "#333333" border.width: 1 @@ -474,16 +505,17 @@ Rectangle { delegate: Rectangle { width: manifestListView.width - height: 28 + height: 36 color: index % 2 === 0 ? "#181818" : "#1e1e1e" Row { anchors.fill: parent anchors.leftMargin: 6 + anchors.rightMargin: 4 spacing: 0 Text { - width: 200 + width: 150 text: modelData["cid"] ?? "" color: "#dddddd" font.pixelSize: 11 @@ -495,7 +527,7 @@ Rectangle { HoverHandler {} } Text { - width: 140 + width: 120 text: modelData["filename"] ?? "" color: "#dddddd" font.pixelSize: 11 @@ -503,7 +535,7 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter } Text { - width: 100 + width: 85 text: modelData["mimetype"] ?? "" color: "#dddddd" font.pixelSize: 11 @@ -511,13 +543,41 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter } Text { - width: 90 + width: 75 text: modelData["datasetSize"] ?? "" color: "#dddddd" font.pixelSize: 11 elide: Text.ElideRight anchors.verticalCenter: parent.verticalCenter } + Row { + spacing: 4 + anchors.verticalCenter: parent.verticalCenter + + Button { + width: 50 + height: 26 + text: "↓" + enabled: root.isRunning() + onClicked: { + root.pendingDownloadManifest = modelData + var filename = modelData["filename"] + || modelData["cid"] || "download" + manifestSaveDialog.currentFile = StandardPaths.writableLocation( + StandardPaths.HomeLocation) + "/" + filename + manifestSaveDialog.open() + } + } + + Button { + width: 50 + height: 26 + text: "🗑" + enabled: root.isRunning() + onClicked: root.backend.remove( + modelData["cid"] ?? "") + } + } } } @@ -531,29 +591,40 @@ Rectangle { } } - // ── Log level section ────────────────────────────────────────────────── - TextField { - id: logLevelField - placeholderTextColor: "#999999" - placeholderText: "Enter the log level to download" - color: "black" - // text: root.downloadCid - onTextChanged: root.logLevel = text + Button { + id: spaceButton + objectName: "spaceButton" + text: "Space" + onClicked: root.backend.space() anchors.top: manifestTableContainer.bottom - anchors.topMargin: 30 + enabled: root.isRunning + anchors.topMargin: 10 anchors.horizontalCenter: parent.horizontalCenter } - Button { - id: logLevelButton - objectName: "logLevelButton" - text: "Log level" - onClicked: root.backend.updateLogLevel(root.logLevel) - anchors.top: logLevelField.bottom - anchors.horizontalCenter: parent.horizontalCenter - enabled: root.isRunning - anchors.topMargin: 10 - } + // ── Log level section ────────────────────────────────────────────────── + // TextField { + // id: logLevelField + // placeholderTextColor: "#999999" + // placeholderText: "Enter the log level to download" + // color: "black" + // // text: root.downloadCid + // onTextChanged: root.logLevel = text + // anchors.top: manifestTableContainer.bottom + // anchors.topMargin: 30 + // anchors.horizontalCenter: parent.horizontalCenter + // } + + // Button { + // id: logLevelButton + // objectName: "logLevelButton" + // text: "Log level" + // onClicked: root.backend.updateLogLevel(root.logLevel) + // anchors.top: logLevelField.bottom + // anchors.horizontalCenter: parent.horizontalCenter + // enabled: root.isRunning + // anchors.topMargin: 10 + // } // TextEdit { // id: selectableText @@ -609,6 +680,22 @@ Rectangle { } } + 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 + } + } + Rectangle { id: debugPanel anchors.left: parent.left From 38d7b0a7e8ff2e811accb446b43cfa71580cf86c Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 17 Feb 2026 13:08:30 +0400 Subject: [PATCH 3/3] Add list --- src/StorageBackend.cpp | 44 ++++++++++++++---- src/StorageBackend.h | 10 ++++ src/qml/StorageView.qml | 100 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 11 deletions(-) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index bbdef1a..bcd1a8d 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -67,6 +67,7 @@ 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); emit startCompleted(); } })) { @@ -156,6 +157,8 @@ LogosResult StorageBackend::init(const QString& configJson = "{}") { m_uploadStatus = "Upload completed!"; emit uploadProgressChanged(); emit uploadStatusChanged(); + + QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); } })) { qWarning() << "StorageWidget: failed to subscribe to storageUploadProgress events"; @@ -550,13 +553,13 @@ void StorageBackend::remove(const QString& cid) { LogosResult result = m_logos->storage_module.remove(cid); if (!result.success) { - debug("StorageBackend::remove failed with error=" + result.getError()); - return; + // 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("Cid " + cid + " removed."); - - // Remove from manifests list + // 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); @@ -564,6 +567,8 @@ void StorageBackend::remove(const QString& cid) { break; } } + + QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); } void StorageBackend::fetch(const QString& cid) { @@ -711,12 +716,33 @@ void StorageBackend::space() { return; } - debug(QString("Space datasetSize %1").arg(result.getInt("totalBlocks"))); - debug(QString("Space quotaMaxBytes %1").arg(result.getInt("quotaMaxBytes"))); - debug(QString("Space quotaUsedBytes %1").arg(result.getInt("quotaUsedBytes"))); - debug(QString("Space quotaReservedBytes %1").arg(result.getInt("quotaReservedBytes"))); + qDebug() << "StorageBackend::space raw value:" << result.value; + + static constexpr qint64 DEFAULT_QUOTA = 20LL * 1024 * 1024 * 1024; // 20 GB + + // Check config for a quota-max-bytes override + qint64 configQuota = 0; + QJsonDocument doc = QJsonDocument::fromJson(m_configJson.toUtf8()); + if (!doc.isNull()) { + configQuota = doc.object().value("quota-max-bytes").toVariant().toLongLong(); + } + + qint64 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)); } +qint64 StorageBackend::quotaMaxBytes() const { return m_quotaMaxBytes; } +qint64 StorageBackend::quotaUsedBytes() const { return m_quotaUsedBytes; } +qint64 StorageBackend::quotaReservedBytes() const { return m_quotaReservedBytes; } + void StorageBackend::updateLogLevel(const QString& logLevel) { qDebug() << "StorageBackend::updateLogLevel called with logLevel=" << logLevel; diff --git a/src/StorageBackend.h b/src/StorageBackend.h index 79fb1ce..0745991 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -34,6 +34,9 @@ class StorageBackend : public QObject { 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 = 0, Starting, Running, Stopping, Destroyed }; @@ -46,6 +49,9 @@ class StorageBackend : public QObject { int uploadProgress() const; QString uploadStatus() const; QVariantList manifests() const; + qint64 quotaMaxBytes() const; + qint64 quotaUsedBytes() const; + qint64 quotaReservedBytes() const; Q_INVOKABLE static QString defaultDataDir(); @@ -91,6 +97,7 @@ class StorageBackend : public QObject { void uploadProgressChanged(); void uploadStatusChanged(); void manifestsChanged(); + void quotaChanged(); private slots: @@ -110,4 +117,7 @@ class StorageBackend : public QObject { qint64 m_uploadTotalBytes = 0; qint64 m_uploadedBytes = 0; QVariantList m_manifests; + qint64 m_quotaMaxBytes = 0; + qint64 m_quotaUsedBytes = 0; + qint64 m_quotaReservedBytes = 0; }; diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index c256c7f..f8976f3 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -128,6 +128,17 @@ Rectangle { function updateLogLevel(logLevel) {} property var manifests: [] + property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 // 20 GB default + property var quotaUsedBytes: 0 + property var quotaReservedBytes: 0 + } + + function formatBytes(bytes) { + 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" } Text { @@ -397,11 +408,96 @@ Rectangle { anchors.horizontalCenter: parent.horizontalCenter } + // ── Disk space bar ──────────────────────────────────────────────────── + Item { + id: spaceBarSection + anchors.top: manifestsTitle.bottom + anchors.topMargin: 10 + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 40 + height: root.backend.quotaMaxBytes > 0 ? 36 : 20 + + readonly property real total: root.backend.quotaMaxBytes + readonly property real used: root.backend.quotaUsedBytes + readonly property real reserved: root.backend.quotaReservedBytes + + // No quota configured + Text { + anchors.centerIn: parent + text: "No quota configured" + color: "#555555" + font.pixelSize: 11 + visible: spaceBarSection.total <= 0 + } + + // Background track + Rectangle { + id: spaceBarTrack + visible: spaceBarSection.total > 0 + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: 14 + radius: 7 + color: "#2a2a2a" + border.color: "#3a3a3a" + border.width: 1 + clip: true + + // Used (green) + Rectangle { + width: Math.min(parent.width * (spaceBarSection.used / spaceBarSection.total), parent.width) + height: parent.height + radius: parent.radius + color: "#4CAF50" + } + + // Reserved (orange), stacked after used + Rectangle { + x: parent.width * (spaceBarSection.used / spaceBarSection.total) + width: Math.min(parent.width * (spaceBarSection.reserved / spaceBarSection.total), + parent.width - x) + height: parent.height + color: "#FF9800" + } + } + + // Labels + Row { + visible: spaceBarSection.total > 0 + anchors.top: spaceBarTrack.bottom + anchors.topMargin: 4 + anchors.horizontalCenter: parent.horizontalCenter + spacing: 16 + + Text { + text: "Used: " + root.formatBytes(spaceBarSection.used) + color: "#4CAF50" + font.pixelSize: 10 + } + Text { + text: "Reserved: " + root.formatBytes(spaceBarSection.reserved) + color: "#FF9800" + font.pixelSize: 10 + } + Text { + text: "Free: " + root.formatBytes(spaceBarSection.total - spaceBarSection.used - spaceBarSection.reserved) + color: "#888888" + font.pixelSize: 10 + } + Text { + text: "Total: " + root.formatBytes(spaceBarSection.total) + color: "#555555" + font.pixelSize: 10 + } + } + } + Row { id: manifestInputRow spacing: 8 - anchors.top: manifestsTitle.bottom - anchors.topMargin: 8 + anchors.top: spaceBarSection.bottom + anchors.topMargin: 16 anchors.horizontalCenter: parent.horizontalCenter TextField {