From e6cd77d8986376cf74bc951ae479811e56e0daa1 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 20 Feb 2026 16:42:33 +0400 Subject: [PATCH 01/19] Add components --- src/qml/ManifestTable.qml | 218 ++++++++++++++++++++++++++++++++++++++ src/qml/SpaceBar.qml | 96 +++++++++++++++++ src/qml/StorageIcon.qml | 14 +++ 3 files changed, 328 insertions(+) create mode 100644 src/qml/ManifestTable.qml create mode 100644 src/qml/SpaceBar.qml create mode 100644 src/qml/StorageIcon.qml diff --git a/src/qml/ManifestTable.qml b/src/qml/ManifestTable.qml new file mode 100644 index 0000000..e3ac830 --- /dev/null +++ b/src/qml/ManifestTable.qml @@ -0,0 +1,218 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +ColumnLayout { + id: root + + property var backend + property bool running: false + + signal downloadRequested(var manifest) + + spacing: Theme.spacing.small + + 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" + } + + // ── Section title ───────────────────────────────────────────────────────── + LogosText { + text: "MANIFESTS" + font.pixelSize: 11 + color: Theme.palette.textTertiary + font.letterSpacing: 1.5 + } + + // ── CID input + fetch button ────────────────────────────────────────────── + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + + LogosTextField { + id: cidInput + Layout.fillWidth: true + placeholderText: "Enter CID to fetch manifest…" + } + + LogosStorageButton { + text: "↓ Fetch" + enabled: root.running && cidInput.text.length > 0 + onClicked: { + root.backend.downloadManifest(cidInput.text) + cidInput.clear() + } + } + } + + // ── Table header ────────────────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + height: 30 + color: Theme.palette.backgroundElevated + radius: 4 + + Row { + 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 } + } + } + + // ── Table body ──────────────────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + height: 240 + color: Theme.palette.background + border.color: Theme.palette.borderSecondary + border.width: 1 + radius: 4 + clip: true + + ListView { + id: manifestList + anchors.fill: parent + model: root.backend ? root.backend.manifests : [] + clip: true + + delegate: Rectangle { + width: manifestList.width + height: 36 + color: index % 2 === 0 ? Theme.palette.background : Theme.palette.backgroundSecondary + + Row { + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 8 + + Text { + width: 160 + 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 } + } + Text { + width: 130 + text: modelData["filename"] ?? "" + color: Theme.palette.textSecondary + font.pixelSize: 11 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 90 + text: modelData["mimetype"] ?? "" + color: Theme.palette.textSecondary + font.pixelSize: 11 + elide: Text.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + Text { + width: 80 + text: root.formatBytes(parseInt(modelData["datasetSize"] ?? "0")) + 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 + color: dlHover.hovered ? Theme.palette.backgroundElevated : "transparent" + border.color: Theme.palette.borderSecondary + border.width: 1 + opacity: root.running ? 1.0 : 0.35 + + Text { + anchors.centerIn: parent + text: "↓" + color: Theme.palette.text + font.pixelSize: 14 + } + HoverHandler { id: dlHover } + MouseArea { + anchors.fill: parent + enabled: root.running + cursorShape: Qt.PointingHandCursor + onClicked: root.downloadRequested(modelData) + } + } + + // Delete + Rectangle { + 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 { + anchors.centerIn: parent + text: "×" + color: Theme.palette.error + font.pixelSize: 16 + font.bold: true + } + HoverHandler { id: rmHover } + MouseArea { + anchors.fill: parent + enabled: root.running + cursorShape: Qt.PointingHandCursor + onClicked: root.backend.remove(modelData["cid"] ?? "") + } + } + } + } + } + + // ── 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 + ] + dotColor: Theme.palette.textMuted + activeOpacity: 0.25 + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: "No manifests yet" + color: Theme.palette.textMuted + font.pixelSize: 12 + Layout.alignment: Qt.AlignHCenter + } + } + } + } +} diff --git a/src/qml/SpaceBar.qml b/src/qml/SpaceBar.qml new file mode 100644 index 0000000..40b37f3 --- /dev/null +++ b/src/qml/SpaceBar.qml @@ -0,0 +1,96 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +ColumnLayout { + id: root + + property real total: 0 + property real used: 0 + property real reserved: 0 + readonly property real free: Math.max(0, total - used - reserved) + + spacing: 8 + + 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" + } + + // ── Section title ───────────────────────────────────────────────────────── + LogosText { + text: "DISK USAGE" + font.pixelSize: 11 + color: Theme.palette.textTertiary + font.letterSpacing: 1.5 + } + + // ── No quota message ────────────────────────────────────────────────────── + LogosText { + text: "No quota configured" + color: Theme.palette.textMuted + font.pixelSize: 12 + visible: root.total <= 0 + } + + // ── Progress track ──────────────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + height: 10 + radius: 5 + color: Theme.palette.backgroundElevated + border.color: Theme.palette.borderSecondary + border.width: 1 + visible: root.total > 0 + clip: true + + // Used (green) + Rectangle { + width: Math.min(parent.width * (root.used / root.total), parent.width) + height: parent.height + radius: parent.radius + color: Theme.palette.success + } + + // Reserved (orange), stacked after used + Rectangle { + x: parent.width * (root.used / root.total) + width: Math.min( + parent.width * (root.reserved / root.total), + parent.width - x) + height: parent.height + color: Theme.palette.warning + } + } + + // ── Legend ──────────────────────────────────────────────────────────────── + Row { + visible: root.total > 0 + spacing: 18 + + Row { + spacing: 5 + Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.success; anchors.verticalCenter: parent.verticalCenter } + Text { text: "Used · " + root.formatBytes(root.used); color: Theme.palette.success; font.pixelSize: 11 } + } + Row { + spacing: 5 + Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.warning; anchors.verticalCenter: parent.verticalCenter } + Text { text: "Reserved · " + root.formatBytes(root.reserved); color: Theme.palette.warning; font.pixelSize: 11 } + } + Row { + spacing: 5 + Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.textSecondary; anchors.verticalCenter: parent.verticalCenter } + Text { text: "Free · " + root.formatBytes(root.free); color: Theme.palette.textSecondary; font.pixelSize: 11 } + } + Row { + spacing: 5 + Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.textMuted; anchors.verticalCenter: parent.verticalCenter } + Text { text: "Total · " + root.formatBytes(root.total); color: Theme.palette.textMuted; font.pixelSize: 11 } + } + } +} diff --git a/src/qml/StorageIcon.qml b/src/qml/StorageIcon.qml new file mode 100644 index 0000000..121e720 --- /dev/null +++ b/src/qml/StorageIcon.qml @@ -0,0 +1,14 @@ +import QtQuick + +// Ring / node pattern — used in the StorageView header +DotIcon { + pattern: [ + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0 + ] + dotSize: 7 + dotSpacing: 5 +} From d5d7bf7af3f2afed1b2f30ac88a268149441aa68 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sat, 21 Feb 2026 07:07:42 +0400 Subject: [PATCH 02/19] Temporary commit --- src/qml/DiskWidget.qml | 88 +++++++++++++++++++++ src/qml/PeersWidget.qml | 79 +++++++++++++++++++ src/qml/PlayIcon.qml | 12 +++ src/qml/SettingsIcon.qml | 12 +++ src/qml/SettingsPopup.qml | 115 +++++++++++++++++++++++++++ src/qml/StopIcon.qml | 12 +++ src/qml/UploadIcon.qml | 12 +++ src/qml/UploadWidget.qml | 159 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 489 insertions(+) create mode 100644 src/qml/DiskWidget.qml create mode 100644 src/qml/PeersWidget.qml create mode 100644 src/qml/PlayIcon.qml create mode 100644 src/qml/SettingsIcon.qml create mode 100644 src/qml/SettingsPopup.qml create mode 100644 src/qml/StopIcon.qml create mode 100644 src/qml/UploadIcon.qml create mode 100644 src/qml/UploadWidget.qml diff --git a/src/qml/DiskWidget.qml b/src/qml/DiskWidget.qml new file mode 100644 index 0000000..a158b4e --- /dev/null +++ b/src/qml/DiskWidget.qml @@ -0,0 +1,88 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +Rectangle { + id: root + + width: 140; height: 140 + radius: 14 + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.borderSecondary + border.width: 1 + + property real total: 0 + property real used: 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" + } + + onTotalChanged: arc.requestPaint() + onUsedChanged: 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 (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() + } + } + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: 2 + + LogosText { + text: root.total > 0 ? root.formatBytes(root.used) : "—" + font.pixelSize: 15 + font.bold: true + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: "STORAGE" + font.pixelSize: 9 + color: Theme.palette.textTertiary + font.letterSpacing: 1.3 + Layout.alignment: Qt.AlignHCenter + } + } +} diff --git a/src/qml/PeersWidget.qml b/src/qml/PeersWidget.qml new file mode 100644 index 0000000..8213b6c --- /dev/null +++ b/src/qml/PeersWidget.qml @@ -0,0 +1,79 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +Rectangle { + 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 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() + } + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: 2 + + LogosText { + text: root.peerCount + font.pixelSize: 22 + font.bold: true + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: "PEERS" + font.pixelSize: 9 + color: Theme.palette.textTertiary + font.letterSpacing: 1.3 + Layout.alignment: Qt.AlignHCenter + } + } +} diff --git a/src/qml/PlayIcon.qml b/src/qml/PlayIcon.qml new file mode 100644 index 0000000..f7dac71 --- /dev/null +++ b/src/qml/PlayIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Right-pointing triangle — play / start +DotIcon { + pattern: [ + 1, 0, 0, 0, 0, + 1, 1, 0, 0, 0, + 1, 1, 1, 0, 0, + 1, 1, 0, 0, 0, + 1, 0, 0, 0, 0 + ] +} diff --git a/src/qml/SettingsIcon.qml b/src/qml/SettingsIcon.qml new file mode 100644 index 0000000..5a735ec --- /dev/null +++ b/src/qml/SettingsIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Settings / gear icon +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 + ] +} diff --git a/src/qml/SettingsPopup.qml b/src/qml/SettingsPopup.qml new file mode 100644 index 0000000..a7b04c7 --- /dev/null +++ b/src/qml/SettingsPopup.qml @@ -0,0 +1,115 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +Popup { + id: root + + property var backend + + modal: true + width: 520 + height: 400 + anchors.centerIn: Overlay.overlay + padding: 24 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.borderSecondary + border.width: 1 + radius: 14 + } + + ColumnLayout { + anchors.fill: parent + spacing: Theme.spacing.small + + LogosText { + text: "Configuration" + font.pixelSize: Theme.typography.titleText + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: "Edit the JSON configuration below, then click Save." + font.pixelSize: Theme.typography.primaryText + color: Theme.palette.textSecondary + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // ── JSON editor ─────────────────────────────────────────────────────── + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Theme.palette.backgroundElevated + radius: 8 + border.color: jsonArea.isValid + ? Theme.palette.borderSecondary : Theme.palette.error + border.width: 1 + + ScrollView { + anchors.fill: parent + anchors.margins: 2 + + TextArea { + id: jsonArea + font.family: "monospace" + font.pixelSize: 12 + color: Theme.palette.text + wrapMode: Text.WrapAnywhere + background: Item {} + + property bool isValid: true + + 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 + + LogosStorageButton { + text: "Cancel" + onClicked: root.close() + } + + LogosStorageButton { + text: "Save" + variant: "success" + enabled: jsonArea.isValid + onClicked: { + root.backend.saveUserConfig(jsonArea.text) + root.backend.reloadIfChanged(jsonArea.text) + root.close() + } + } + } + } +} diff --git a/src/qml/StopIcon.qml b/src/qml/StopIcon.qml new file mode 100644 index 0000000..e531f3b --- /dev/null +++ b/src/qml/StopIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Filled square — stop +DotIcon { + pattern: [ + 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, + 0, 1, 1, 1, 0, + 0, 1, 1, 1, 0, + 0, 0, 0, 0, 0 + ] +} diff --git a/src/qml/UploadIcon.qml b/src/qml/UploadIcon.qml new file mode 100644 index 0000000..dca846e --- /dev/null +++ b/src/qml/UploadIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Upward arrow — upload +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0 + ] +} diff --git a/src/qml/UploadWidget.qml b/src/qml/UploadWidget.qml new file mode 100644 index 0000000..154843e --- /dev/null +++ b/src/qml/UploadWidget.qml @@ -0,0 +1,159 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +Rectangle { + id: root + + width: 140; height: 140 + radius: 14 + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.borderSecondary + border.width: 1 + + property int uploadProgress: 0 // 0‒100 + property string uploadCid: "" + property bool running: false + + // States + readonly property bool isUploading: uploadProgress > 0 && uploadProgress < 100 + readonly property bool isDone: uploadCid.length > 0 + + signal uploadRequested + + onUploadProgressChanged: arc.requestPaint() + onUploadCidChanged: arc.requestPaint() + + // ── Arc ─────────────────────────────────────────────────────────────────── + 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 + var fraction = root.isDone ? 1.0 : root.uploadProgress / 100.0 + if (fraction > 0) { + ctx.beginPath() + ctx.arc(cx, cy, r, startRad, startRad + totalRad * fraction) + ctx.strokeStyle = root.isDone + ? Theme.palette.success.toString() + : Theme.palette.text.toString() + ctx.lineWidth = lw + ctx.lineCap = "round" + ctx.stroke() + } + } + } + + // ── Center content ──────────────────────────────────────────────────────── + ColumnLayout { + anchors.centerIn: parent + spacing: 2 + + // Idle: dot upload icon + UploadIcon { + dotColor: Theme.palette.textSecondary + dotSize: 4; dotSpacing: 3 + activeOpacity: 0.5 + visible: !root.isUploading && !root.isDone + Layout.alignment: Qt.AlignHCenter + } + + // Uploading: percentage + LogosText { + text: root.uploadProgress + "%" + font.pixelSize: 22 + font.bold: true + visible: root.isUploading + Layout.alignment: Qt.AlignHCenter + } + + // Done: abbreviated CID + LogosText { + text: root.uploadCid.length > 10 + ? root.uploadCid.substring(0, 6) + "…" + root.uploadCid.slice(-4) + : root.uploadCid + font.pixelSize: 11 + font.family: "monospace" + visible: root.isDone && !root.isUploading + Layout.alignment: Qt.AlignHCenter + } + + LogosText { + text: root.isDone ? "TAP TO COPY" : "UPLOAD" + font.pixelSize: 9 + color: Theme.palette.textTertiary + font.letterSpacing: 1.2 + Layout.alignment: Qt.AlignHCenter + } + } + + // ── "Copied!" toast ─────────────────────────────────────────────────────── + Rectangle { + id: copiedToast + anchors.centerIn: parent + width: 80; height: 26 + radius: 6 + color: Theme.palette.backgroundElevated + border.color: Theme.palette.success + border.width: 1 + visible: false + + LogosText { + anchors.centerIn: parent + text: "Copied ✓" + font.pixelSize: 11 + color: Theme.palette.success + } + } + + SequentialAnimation { + id: copiedAnim + ScriptAction { script: copiedToast.visible = true } + PauseAnimation{ duration: 1400 } + ScriptAction { script: copiedToast.visible = false } + } + + // ── Click handler ───────────────────────────────────────────────────────── + HoverHandler { id: widgetHover } + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: widgetHover.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent" + } + + MouseArea { + anchors.fill: parent + cursorShape: (root.running || root.isDone) ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + if (root.isDone) { + Qt.copyToClipboard(root.uploadCid) + copiedAnim.restart() + } else if (root.running) { + root.uploadRequested() + } + } + } +} From 4db25d4c24e404623354cac84a208dac8ff820ec Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sat, 21 Feb 2026 07:27:12 +0400 Subject: [PATCH 03/19] Add qml files --- src/qml/ArcGauge.qml | 56 ++++++++++++++++++++++++++++++++++++++++ src/qml/DownloadIcon.qml | 12 +++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/qml/ArcGauge.qml create mode 100644 src/qml/DownloadIcon.qml diff --git a/src/qml/ArcGauge.qml b/src/qml/ArcGauge.qml new file mode 100644 index 0000000..0b997f0 --- /dev/null +++ b/src/qml/ArcGauge.qml @@ -0,0 +1,56 @@ +import QtQuick +import Logos.Theme + +// Reusable arc gauge — same visual style as the Nothing OS ring widgets. +// +// Usage: +// ArcGauge { +// anchors.fill: parent +// fraction: 0.65 +// fillColor: Theme.palette.success +// } +Canvas { + id: root + + // 0.0 – 1.0 (clamped internally) + property real fraction: 0.0 + property color trackColor: Theme.palette.textMuted + property color fillColor: Theme.palette.text + property real arcRadius: 46 + property real arcWidth: 8 + + onFractionChanged: requestPaint() + onTrackColorChanged: requestPaint() + onFillColorChanged: requestPaint() + + Component.onCompleted: requestPaint() + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + var cx = width / 2 + var cy = height / 2 + var startRad = 130 * Math.PI / 180 + var totalRad = 280 * Math.PI / 180 + + // ── Track (full arc, muted) ─────────────────────────────────────────── + ctx.beginPath() + ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad) + ctx.strokeStyle = root.trackColor.toString() + ctx.lineWidth = root.arcWidth + ctx.lineCap = "round" + ctx.stroke() + + // ── Fill (proportional) ─────────────────────────────────────────────── + var f = Math.min(Math.max(root.fraction, 0.0), 1.0) + if (f > 0) { + ctx.beginPath() + ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad * f) + ctx.strokeStyle = root.fillColor.toString() + ctx.lineWidth = root.arcWidth + ctx.lineCap = "round" + ctx.stroke() + } + } +} diff --git a/src/qml/DownloadIcon.qml b/src/qml/DownloadIcon.qml new file mode 100644 index 0000000..f6e9c2e --- /dev/null +++ b/src/qml/DownloadIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// Downward arrow — download (mirrored UploadIcon) +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 0 + ] +} From 52490bfcdf254910d6167852f4c77d934e3ae330 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sat, 21 Feb 2026 07:49:47 +0400 Subject: [PATCH 04/19] Add delete icon --- src/qml/DeleteIcon.qml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/qml/DeleteIcon.qml diff --git a/src/qml/DeleteIcon.qml b/src/qml/DeleteIcon.qml new file mode 100644 index 0000000..2886705 --- /dev/null +++ b/src/qml/DeleteIcon.qml @@ -0,0 +1,12 @@ +import QtQuick + +// X / delete icon +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 + ] +} From 67dd5ed957f08e0ddb440331c943733b5e47228f Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 22 Feb 2026 07:36:20 +0400 Subject: [PATCH 05/19] Fix wrong path --- .gitmodules | 2 +- logos-design-system | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 160000 logos-design-system diff --git a/.gitmodules b/.gitmodules index 0d3934b..9a8b527 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,6 +10,6 @@ [submodule "vendor/logos-storage-nim"] path = vendor/logos-storage-nim url = https://github.com/logos-storage/logos-storage-nim -[submodule "logos-design-system"] +[submodule "vendor/logos-design-system"] path = logos-design-system url = https://github.com/logos-co/logos-design-system diff --git a/logos-design-system b/logos-design-system deleted file mode 160000 index 596811c..0000000 --- a/logos-design-system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 596811cbb0a0644322267368e87fab80e34203d8 From 1ff471a35cddcc64392dcffd34ea99b651c77e73 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 22 Feb 2026 08:46:59 +0400 Subject: [PATCH 06/19] Move components --- .gitmodules | 2 +- src/qml/ArcWidget.qml | 46 ++ src/qml/JsonEditor.qml | 53 ++ src/qml/SpaceBar.qml | 96 --- src/qml/StartNode.qml | 1 - src/qml/StorageView.qml | 1263 +++++++++++------------------- src/qml/UploadWidget.qml | 127 +-- src/qml/icons/AdvancedIcon.qml | 17 + src/qml/icons/ArcGauge.qml | 56 ++ src/qml/icons/ArcWidget.qml | 76 ++ src/qml/icons/DeleteIcon.qml | 18 + src/qml/icons/DotIcon.qml | 74 ++ src/qml/icons/DownloadIcon.qml | 17 + src/qml/icons/GuideIcon.qml | 17 + src/qml/icons/NodeStatusIcon.qml | 72 ++ src/qml/icons/PlayIcon.qml | 17 + src/qml/icons/PortIcon.qml | 17 + src/qml/icons/SettingsIcon.qml | 11 + src/qml/icons/StopIcon.qml | 17 + src/qml/icons/StorageIcon.qml | 19 + src/qml/icons/UploadIcon.qml | 17 + src/qml/icons/UpnpIcon.qml | 17 + vendor/logos-design-system | 1 + 23 files changed, 1042 insertions(+), 1009 deletions(-) create mode 100644 src/qml/ArcWidget.qml create mode 100644 src/qml/JsonEditor.qml delete mode 100644 src/qml/SpaceBar.qml create mode 100644 src/qml/icons/AdvancedIcon.qml create mode 100644 src/qml/icons/ArcGauge.qml create mode 100644 src/qml/icons/ArcWidget.qml create mode 100644 src/qml/icons/DeleteIcon.qml create mode 100644 src/qml/icons/DotIcon.qml create mode 100644 src/qml/icons/DownloadIcon.qml create mode 100644 src/qml/icons/GuideIcon.qml create mode 100644 src/qml/icons/NodeStatusIcon.qml create mode 100644 src/qml/icons/PlayIcon.qml create mode 100644 src/qml/icons/PortIcon.qml create mode 100644 src/qml/icons/SettingsIcon.qml create mode 100644 src/qml/icons/StopIcon.qml create mode 100644 src/qml/icons/StorageIcon.qml create mode 100644 src/qml/icons/UploadIcon.qml create mode 100644 src/qml/icons/UpnpIcon.qml create mode 160000 vendor/logos-design-system diff --git a/.gitmodules b/.gitmodules index 9a8b527..d0199ef 100644 --- a/.gitmodules +++ b/.gitmodules @@ -11,5 +11,5 @@ path = vendor/logos-storage-nim url = https://github.com/logos-storage/logos-storage-nim [submodule "vendor/logos-design-system"] - path = logos-design-system + path = vendor/logos-design-system url = https://github.com/logos-co/logos-design-system diff --git a/src/qml/ArcWidget.qml b/src/qml/ArcWidget.qml new file mode 100644 index 0000000..820e0e6 --- /dev/null +++ b/src/qml/ArcWidget.qml @@ -0,0 +1,46 @@ +import QtQuick +import Logos.Theme + +// Reusable ring widget — Rectangle + ArcGauge + content overlay. +// +// Usage: +// ArcWidget { +// fraction: 0.65 +// fillColor: Theme.palette.success +// +// ColumnLayout { anchors.centerIn: parent; ... } +// } +// +// Children are placed inside an overlay Item that fills the widget, +// so anchors such as `anchors.centerIn: parent` work as expected. +Rectangle { + id: root + + width: 140 + height: 140 + radius: 14 + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.borderSecondary + border.width: 1 + + // ── Arc properties ──────────────────────────────────────────────────────── + property real fraction: 0.0 + property color fillColor: Theme.palette.text + property color trackColor: Theme.palette.textMuted + + // ── Content slot ────────────────────────────────────────────────────────── + // Children declared inside ArcWidget { … } land here, on top of the arc. + default property alias content: overlay.data + + ArcGauge { + anchors.fill: parent + fraction: root.fraction + trackColor: root.trackColor + fillColor: root.fillColor + } + + Item { + id: overlay + anchors.fill: parent + } +} diff --git a/src/qml/JsonEditor.qml b/src/qml/JsonEditor.qml new file mode 100644 index 0000000..59fa7b7 --- /dev/null +++ b/src/qml/JsonEditor.qml @@ -0,0 +1,53 @@ +import QtQuick +import QtQuick.Controls +import Logos.Theme + +// Reusable JSON editor with live validation. +// Usage: +// JsonEditor { +// id: editor +// Layout.fillWidth: true +// Layout.fillHeight: true +// } +// // Load content (e.g. when a popup opens): +// editor.load(backend.configJson() || "{}") +// // Read back: +// editor.text // current text +// editor.isValid // false when JSON.parse would throw +Rectangle { + id: root + + property alias text: jsonArea.text + property bool isValid: true + + Component.onCompleted: root.validate() + + color: Theme.palette.backgroundElevated + radius: 8 + border.color: root.isValid ? Theme.palette.borderSecondary : Theme.palette.error + border.width: 1 + + function validate() { + try { + JSON.parse(jsonArea.text) + isValid = true + } catch (e) { + isValid = false + } + } + + 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 {} + onTextChanged: root.validate() + } + } +} diff --git a/src/qml/SpaceBar.qml b/src/qml/SpaceBar.qml deleted file mode 100644 index 40b37f3..0000000 --- a/src/qml/SpaceBar.qml +++ /dev/null @@ -1,96 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Logos.Theme -import Logos.Controls - -ColumnLayout { - id: root - - property real total: 0 - property real used: 0 - property real reserved: 0 - readonly property real free: Math.max(0, total - used - reserved) - - spacing: 8 - - 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" - } - - // ── Section title ───────────────────────────────────────────────────────── - LogosText { - text: "DISK USAGE" - font.pixelSize: 11 - color: Theme.palette.textTertiary - font.letterSpacing: 1.5 - } - - // ── No quota message ────────────────────────────────────────────────────── - LogosText { - text: "No quota configured" - color: Theme.palette.textMuted - font.pixelSize: 12 - visible: root.total <= 0 - } - - // ── Progress track ──────────────────────────────────────────────────────── - Rectangle { - Layout.fillWidth: true - height: 10 - radius: 5 - color: Theme.palette.backgroundElevated - border.color: Theme.palette.borderSecondary - border.width: 1 - visible: root.total > 0 - clip: true - - // Used (green) - Rectangle { - width: Math.min(parent.width * (root.used / root.total), parent.width) - height: parent.height - radius: parent.radius - color: Theme.palette.success - } - - // Reserved (orange), stacked after used - Rectangle { - x: parent.width * (root.used / root.total) - width: Math.min( - parent.width * (root.reserved / root.total), - parent.width - x) - height: parent.height - color: Theme.palette.warning - } - } - - // ── Legend ──────────────────────────────────────────────────────────────── - Row { - visible: root.total > 0 - spacing: 18 - - Row { - spacing: 5 - Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.success; anchors.verticalCenter: parent.verticalCenter } - Text { text: "Used · " + root.formatBytes(root.used); color: Theme.palette.success; font.pixelSize: 11 } - } - Row { - spacing: 5 - Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.warning; anchors.verticalCenter: parent.verticalCenter } - Text { text: "Reserved · " + root.formatBytes(root.reserved); color: Theme.palette.warning; font.pixelSize: 11 } - } - Row { - spacing: 5 - Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.textSecondary; anchors.verticalCenter: parent.verticalCenter } - Text { text: "Free · " + root.formatBytes(root.free); color: Theme.palette.textSecondary; font.pixelSize: 11 } - } - Row { - spacing: 5 - Rectangle { width: 7; height: 7; radius: 2; color: Theme.palette.textMuted; anchors.verticalCenter: parent.verticalCenter } - Text { text: "Total · " + root.formatBytes(root.total); color: Theme.palette.textMuted; font.pixelSize: 11 } - } - } -} diff --git a/src/qml/StartNode.qml b/src/qml/StartNode.qml index 7c466fb..0e99a74 100644 --- a/src/qml/StartNode.qml +++ b/src/qml/StartNode.qml @@ -1,6 +1,5 @@ import QtQuick import QtQuick.Layouts -import QtQuick.Controls import Logos.Controls import Logos.Theme diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index 78a55a0..3ee0cd7 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -3,6 +3,8 @@ import QtQuick.Controls import QtQuick.Dialogs import QtQuick.Layouts import QtCore +import Logos.Theme +import Logos.Controls // qmllint disable unqualified LogosStorageLayout { @@ -14,727 +16,106 @@ LogosStorageLayout { readonly property int running: 2 readonly property int stopping: 3 readonly property int destroyed: 4 - property string peerId: "" - property string downloadDestination: "" - property url downloadCid: "" - property string logLevel: "" - property bool showDebug: false + property int peerCount: 0 property var pendingDownloadManifest: null - property url uploadCid: root.backend.cid - property url configJson: root.backend.configJson + property bool showDebug: false + + function isRunning() { + return backend.status === running + } function getStatusLabel() { switch (backend.status) { case stopped: - return "Logos Storage stopped." + return "Stopped" case starting: - return "Logos Storage is starting..." + return "Starting…" case running: - return "Logos Storage started." + return "Running" case stopping: - return "Logos Storage is stopping..." + return "Stopping…" case destroyed: - return "Logos Storage is not initialised." + return "Not initialised" + default: + return "" } } - function startStopText() { - if (backend.status == running) { - return "Stop" - } - return "Start" - } - - function canStartStop() { - return backend.status == running || backend.status == stopped - } - - function isRunning() { - return backend.status == running - } - Component.onCompleted: root.backend.start() + HealthIndicator { + id: health + 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 debugLogs: "Hello !" + property var debugLogs: "Hello!" property var configJson: "{}" property string uploadStatus: "" property int uploadProgress: 0 property var manifests: [] - property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 // 20 GB default + property var quotaMaxBytes: 20 * 1024 * 1024 * 1024 property var quotaUsedBytes: 0 - property var quotaReservedBytes: 0 + property string cid: "" - function start(newConfigJson) { + signal nodeIsUp + signal nodeIsntUp(string reason) + signal peersUpdated(int count) + + function start() { status = root.running } - function stop() { status = root.stopped } - } - - 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" - } - - HealthIndicator { - backend: root.backend - anchors.fill: parent - } - - Text { - id: statusTextElement - objectName: "status" - text: root.getStatusLabel() - color: "white" - font.pointSize: 20 - anchors.top: parent.top - anchors.topMargin: 20 - anchors.horizontalCenter: parent.horizontalCenter - } - - Button { - property var isStopped: root.backend.status == root.stopped - - id: startStopButton - objectName: "startStopButton" - anchors.leftMargin: 50 - text: root.startStopText() - enabled: root.canStartStop() - onClicked: isStopped ? root.backend.start( - jsonEditor.text) : root.backend.stop() - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: statusTextElement.bottom - anchors.topMargin: 10 - } - - TextEdit { - id: cidTextEdit - objectName: "cid" - color: "white" - font.pointSize: 14 - readOnly: true - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: startStopButton.bottom - anchors.topMargin: 10 - text: root.uploadCid - } - - Button { - id: openFile - text: "Open file" - onClicked: fileDialog.open() - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: cidTextEdit.bottom - anchors.topMargin: 15 - enabled: root.isRunning - } - - Column { - id: uploadProgressColumn - anchors.top: openFile.bottom - anchors.topMargin: 10 - anchors.horizontalCenter: parent.horizontalCenter - width: 300 - spacing: 5 - visible: root.backend.uploadProgress > 0 - - ProgressBar { - width: parent.width - value: root.backend.uploadProgress / 100.0 - - background: Rectangle { - color: "#333333" - radius: 3 - implicitWidth: 300 - implicitHeight: 6 - } - - contentItem: Item { - implicitWidth: 300 - implicitHeight: 6 - - Rectangle { - width: parent.width * control.visualPosition - height: parent.height - radius: 3 - color: "#4CAF50" - } - } + function checkNodeIsUp() {} + function tryUploadFile(f) {} + function downloadManifest(c) {} + function remove(c) {} + function tryDownloadFile(c, d) {} + function space() {} + function tryDebug() {} + function showPeerId() {} + function dataDir() {} + function spr() {} + function version() {} + function saveUserConfig(j) {} + function reloadIfChanged(j) {} + function configJson() { + return "{}" } - - Text { - text: root.backend.uploadStatus - color: "#888888" - font.pixelSize: 10 - anchors.horizontalCenter: parent.horizontalCenter + function peerCount() { + return 0 } } - // 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: uploadProgressColumn.bottom - anchors.horizontalCenter: parent.horizontalCenter - enabled: root.isRunning - anchors.topMargin: 50 - } - - Button { - id: peerIdButton - objectName: "peerIdButton" - text: "Peer Id" - onClicked: root.backend.showPeerId() - anchors.top: uploadProgressColumn.bottom - anchors.right: debugButton.left - enabled: root.isRunning - anchors.topMargin: 50 - } - - Button { - id: dataDirButton - objectName: "dataDirButton" - text: "Data dir" - onClicked: root.backend.dataDir() - anchors.top: uploadProgressColumn.bottom - anchors.right: peerIdButton.left - enabled: root.isRunning - anchors.topMargin: 50 - } - - Button { - id: sprButton - objectName: "sprButton" - text: "SPR" - onClicked: root.backend.spr() - anchors.top: uploadProgressColumn.bottom - anchors.left: debugButton.right - enabled: root.isRunning - anchors.topMargin: 50 - } - - Button { - id: versionButton - objectName: "versionButton" - text: "Version" - onClicked: root.backend.version() - 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 - // } - - // 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: 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: 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: downloadManifestsButton - objectName: "downloadManifestsButton" - text: "Manifests" - onClicked: root.backend.downloadManifests() - anchors.top: cidDownloadButton.bottom - anchors.horizontalCenter: parent.horizontalCenter - enabled: root.isRunning - anchors.topMargin: 10 - }*/ - - // ── Manifests section ────────────────────────────────────────────────── - Text { - id: manifestsTitle - text: "Manifests" - color: "white" - font.pixelSize: 14 - font.bold: true - anchors.top: versionButton.bottom - anchors.topMargin: 30 - 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: spaceBarSection.bottom - anchors.topMargin: 16 - 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.horizontalCenter: parent.horizontalCenter - width: parent.width - 40 - height: 28 - color: "#222222" - radius: 2 - - Row { - anchors.fill: parent - anchors.leftMargin: 6 - - 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 - } - } - } - - Rectangle { - id: manifestTableContainer - anchors.top: manifestTableHeader.bottom - anchors.left: manifestTableHeader.left - anchors.right: manifestTableHeader.right - height: 280 - 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: 36 - color: index % 2 === 0 ? "#181818" : "#1e1e1e" - - Row { - anchors.fill: parent - anchors.leftMargin: 6 - anchors.rightMargin: 4 - spacing: 0 - - Text { - width: 150 - 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: 120 - text: modelData["filename"] ?? "" - color: "#dddddd" - font.pixelSize: 11 - elide: Text.ElideRight - anchors.verticalCenter: parent.verticalCenter - } - Text { - width: 85 - text: modelData["mimetype"] ?? "" - color: "#dddddd" - font.pixelSize: 11 - elide: Text.ElideRight - anchors.verticalCenter: parent.verticalCenter - } - Text { - 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"] ?? "") - } - } - } - } - - Text { - anchors.centerIn: parent - text: "No manifests yet" - color: "#555555" - font.pixelSize: 12 - visible: manifestListView.count === 0 - } - } - } - - Button { - id: spaceButton - objectName: "spaceButton" - text: "Space" - onClicked: root.backend.space() - anchors.top: manifestTableContainer.bottom - enabled: root.isRunning - anchors.topMargin: 10 - anchors.horizontalCenter: parent.horizontalCenter - } - - // ── 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 - // anchors.fill: parent - // anchors.margins: 10 - // text: "This text is selectable. You can copy it, but not edit it." - // readOnly: true // Makes the text non-editable - // selectByMouse: true // Enables selection by mouse drag (often the default for desktop) - // // Optional: Change cursor shape to IBeam when hovering - // MouseArea { - // anchors.fill: parent - // cursorShape: Qt.IBeamCursor - // acceptedButtons: Qt.NoButton // Allows TextEdit to handle mouse events - // } - // } - - // Button { - // anchors.left: parent.left - // anchors.bottom: parent.bottom - // objectName: "uploadButton" - // text: "Upload" - // anchors.bottomMargin: 80 - // onClicked: root.backend.tryUpload() - // } - - // Button { - // anchors.left: parent.left - // anchors.bottom: parent.bottom - // objectName: "finalizeButton" - // text: "Finalize" - // onClicked: root.backend.tryUploadFinalize() - // } - // Button { - // anchors.left: parent.left - // anchors.bottom: parent.bottom - // objectName: "uploadFileButton" - // text: "Upload file" - // onClicked: root.backend.tryUploadFile() - // anchors.bottomMargin: 30 - // } + // ── File dialogs ────────────────────────────────────────────────────────── FileDialog { id: fileDialog onAccepted: root.backend.tryUploadFile(fileDialog.selectedFile) } - FileDialog { - id: fileDialog2 - fileMode: FileDialog.SaveFile - onAccepted: { - root.downloadDestination = fileDialog2.selectedFile - console.log("Destination selected:", - root.backend.downloadDestination) - } - } - FileDialog { id: manifestSaveDialog fileMode: FileDialog.SaveFile @@ -746,8 +127,327 @@ LogosStorageLayout { root.pendingDownloadManifest = null } } - onRejected: { - 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 + anchors.bottomMargin: root.showDebug ? debugPanel.height : 0 + contentWidth: availableWidth + clip: true + + ColumnLayout { + width: mainScroll.availableWidth + spacing: 0 + + // ══════════════════════════════════════════════════════════════════ + // Header — node identity + settings + start/stop + // ══════════════════════════════════════════════════════════════════ + RowLayout { + 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() + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 24 + Layout.rightMargin: 24 + Layout.preferredHeight: 1 + color: Theme.palette.borderSecondary + } + + RowLayout { + 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() + } + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.leftMargin: 24 + Layout.rightMargin: 24 + Layout.preferredHeight: 1 + color: Theme.palette.borderSecondary + } + + ManifestTable { + Layout.fillWidth: true + Layout.leftMargin: 24 + Layout.rightMargin: 24 + Layout.topMargin: 20 + 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 { + Layout.preferredHeight: 20 + } } } @@ -756,133 +456,92 @@ LogosStorageLayout { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - height: 150 - color: "#111111" - visible: root.showDebug // or: visible: showDebug + height: 220 + color: Theme.palette.backgroundElevated + border.color: Theme.palette.borderSecondary + border.width: 1 + visible: root.showDebug - TabBar { - id: bar - width: parent.width + ColumnLayout { + anchors.fill: parent + spacing: 0 - TabButton { - text: qsTr("Logs") - } + // Dev action buttons + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.topMargin: 6 + Layout.bottomMargin: 4 + spacing: 6 - TabButton { - text: qsTr("Config") - } - } - - StackLayout { - id: stackLayout - currentIndex: bar.currentIndex - anchors.top: bar.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - - Item { - id: homeTab - - Flickable { - id: flick - anchors.fill: parent - clip: true - - contentWidth: width - contentHeight: debugText.paintedHeight - - TextEdit { - id: debugText - width: flick.width - text: root.backend.debugLogs - color: "#dddddd" - font.family: "monospace" - font.pixelSize: 12 - wrapMode: Text.WrapAnywhere - readOnly: true - - onTextChanged: Qt.callLater(function () { - flick.contentY = Math.max( - 0, flick.contentHeight - flick.height) - }) - } + 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 { - id: discoverTab + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Theme.palette.borderSecondary + } - ScrollView { - anchors.fill: parent + // Logs + Flickable { + id: logFlick + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + contentWidth: width + contentHeight: debugText.paintedHeight - TextArea { - id: jsonEditor - font.family: "monospace" - font.pixelSize: 12 - color: "#d4d4d4" - width: parent.width - height: parent.height - wrapMode: Text.WrapAnywhere + 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 - background: Rectangle { - color: "#1e1e1e" - border.color: jsonEditor.isValid ? "#3a3a3a" : "#ff0000" - border.width: 1 - } - - property bool isValid: true - - Connections { - target: root.backend - - function onConfigJsonChanged() { - jsonEditor.text = root.backend.configJson - try { - const jsonData = JSON.parse(jsonEditor.text) - jsonEditor.isValid = true - } catch (e) { - jsonEditor.isValid = false - } - } - } - - Component.onCompleted: { - text = root.backend.configJson - - try { - const jsonData = JSON.parse(text) - isValid = true - } catch (e) { - isValid = false - } - } - - onTextChanged: { - - // try { - // const jsonData = JSON.parse(text) - // isValid = true - // } catch (e) { - // isValid = false - // } - } - - onEditingFinished: { - try { - const jsonData = JSON.parse(text) - root.backend.saveUserConfig(text) - isValid = true - } catch (e) { - isValid = false - } - } - } + onTextChanged: Qt.callLater(function () { + logFlick.contentY = Math.max( + 0, logFlick.contentHeight - logFlick.height) + }) } } } - Shortcut { - sequence: "Ctrl+D" - onActivated: root.showDebug = !root.showDebug - } } } diff --git a/src/qml/UploadWidget.qml b/src/qml/UploadWidget.qml index 154843e..44a806c 100644 --- a/src/qml/UploadWidget.qml +++ b/src/qml/UploadWidget.qml @@ -3,80 +3,32 @@ 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 uploadProgress: 0 // 0–100 + property bool running: false - property int uploadProgress: 0 // 0‒100 - property string uploadCid: "" - property bool running: false - - // States readonly property bool isUploading: uploadProgress > 0 && uploadProgress < 100 - readonly property bool isDone: uploadCid.length > 0 + readonly property bool isDone: uploadProgress >= 100 signal uploadRequested - onUploadProgressChanged: arc.requestPaint() - onUploadCidChanged: arc.requestPaint() - - // ── Arc ─────────────────────────────────────────────────────────────────── - 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 - var fraction = root.isDone ? 1.0 : root.uploadProgress / 100.0 - if (fraction > 0) { - ctx.beginPath() - ctx.arc(cx, cy, r, startRad, startRad + totalRad * fraction) - ctx.strokeStyle = root.isDone - ? Theme.palette.success.toString() - : Theme.palette.text.toString() - ctx.lineWidth = lw - ctx.lineCap = "round" - ctx.stroke() - } - } - } + fraction: root.uploadProgress / 100.0 + fillColor: root.isDone ? Theme.palette.success : Theme.palette.text // ── Center content ──────────────────────────────────────────────────────── ColumnLayout { anchors.centerIn: parent spacing: 2 - // Idle: dot upload icon + // 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 && !root.isDone + visible: !root.isUploading Layout.alignment: Qt.AlignHCenter } @@ -89,19 +41,8 @@ Rectangle { Layout.alignment: Qt.AlignHCenter } - // Done: abbreviated CID LogosText { - text: root.uploadCid.length > 10 - ? root.uploadCid.substring(0, 6) + "…" + root.uploadCid.slice(-4) - : root.uploadCid - font.pixelSize: 11 - font.family: "monospace" - visible: root.isDone && !root.isUploading - Layout.alignment: Qt.AlignHCenter - } - - LogosText { - text: root.isDone ? "TAP TO COPY" : "UPLOAD" + text: root.isDone ? "DONE" : "UPLOAD" font.pixelSize: 9 color: Theme.palette.textTertiary font.letterSpacing: 1.2 @@ -109,51 +50,19 @@ Rectangle { } } - // ── "Copied!" toast ─────────────────────────────────────────────────────── - Rectangle { - id: copiedToast - anchors.centerIn: parent - width: 80; height: 26 - radius: 6 - color: Theme.palette.backgroundElevated - border.color: Theme.palette.success - border.width: 1 - visible: false - - LogosText { - anchors.centerIn: parent - text: "Copied ✓" - font.pixelSize: 11 - color: Theme.palette.success - } - } - - SequentialAnimation { - id: copiedAnim - ScriptAction { script: copiedToast.visible = true } - PauseAnimation{ duration: 1400 } - ScriptAction { script: copiedToast.visible = false } - } - - // ── Click handler ───────────────────────────────────────────────────────── + // ── Hover overlay ───────────────────────────────────────────────────────── HoverHandler { id: widgetHover } Rectangle { anchors.fill: parent - radius: parent.radius - color: widgetHover.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent" + radius: root.radius + 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 || root.isDone) ? Qt.PointingHandCursor : Qt.ArrowCursor - onClicked: { - if (root.isDone) { - Qt.copyToClipboard(root.uploadCid) - copiedAnim.restart() - } else if (root.running) { - root.uploadRequested() - } - } + cursorShape: root.running ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: if (root.running) root.uploadRequested() } } diff --git a/src/qml/icons/AdvancedIcon.qml b/src/qml/icons/AdvancedIcon.qml new file mode 100644 index 0000000..66b882e --- /dev/null +++ b/src/qml/icons/AdvancedIcon.qml @@ -0,0 +1,17 @@ +import QtQuick + +// X pattern — advanced / expert mode +// ● . . . ● +// . ● . ● . +// . . ● . . +// . ● . ● . +// ● . . . ● +DotIcon { + pattern: [ + 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, + 1, 0, 0, 0, 1 + ] +} diff --git a/src/qml/icons/ArcGauge.qml b/src/qml/icons/ArcGauge.qml new file mode 100644 index 0000000..0b997f0 --- /dev/null +++ b/src/qml/icons/ArcGauge.qml @@ -0,0 +1,56 @@ +import QtQuick +import Logos.Theme + +// Reusable arc gauge — same visual style as the Nothing OS ring widgets. +// +// Usage: +// ArcGauge { +// anchors.fill: parent +// fraction: 0.65 +// fillColor: Theme.palette.success +// } +Canvas { + id: root + + // 0.0 – 1.0 (clamped internally) + property real fraction: 0.0 + property color trackColor: Theme.palette.textMuted + property color fillColor: Theme.palette.text + property real arcRadius: 46 + property real arcWidth: 8 + + onFractionChanged: requestPaint() + onTrackColorChanged: requestPaint() + onFillColorChanged: requestPaint() + + Component.onCompleted: requestPaint() + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + var cx = width / 2 + var cy = height / 2 + var startRad = 130 * Math.PI / 180 + var totalRad = 280 * Math.PI / 180 + + // ── Track (full arc, muted) ─────────────────────────────────────────── + ctx.beginPath() + ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad) + ctx.strokeStyle = root.trackColor.toString() + ctx.lineWidth = root.arcWidth + ctx.lineCap = "round" + ctx.stroke() + + // ── Fill (proportional) ─────────────────────────────────────────────── + var f = Math.min(Math.max(root.fraction, 0.0), 1.0) + if (f > 0) { + ctx.beginPath() + ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad * f) + ctx.strokeStyle = root.fillColor.toString() + ctx.lineWidth = root.arcWidth + ctx.lineCap = "round" + ctx.stroke() + } + } +} diff --git a/src/qml/icons/ArcWidget.qml b/src/qml/icons/ArcWidget.qml new file mode 100644 index 0000000..0b1bc77 --- /dev/null +++ b/src/qml/icons/ArcWidget.qml @@ -0,0 +1,76 @@ +import QtQuick +import Logos.Theme + +// Reusable ring widget — Rectangle + arc canvas + content overlay. +// Usage: +// ArcWidget { +// fraction: 0.65 +// fillColor: Theme.palette.success +// ColumnLayout { anchors.centerIn: parent; ... } +// } +// Children are placed inside an overlay Item that fills the widget, +// so anchors such as `anchors.centerIn: parent` work as expected. +Rectangle { + id: root + + width: 140 + height: 140 + radius: 14 + color: Theme.palette.backgroundSecondary + border.color: Theme.palette.borderSecondary + border.width: 1 + + property real fraction: 0.0 + property color fillColor: Theme.palette.text + property color trackColor: Theme.palette.textMuted + property real arcRadius: 46 + property real arcWidth: 8 + + onFractionChanged: arc.requestPaint() + onFillColorChanged: arc.requestPaint() + onTrackColorChanged: arc.requestPaint() + + default property alias content: overlay.data + + 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 startRad = 130 * Math.PI / 180 + var totalRad = 280 * Math.PI / 180 + + // Track (full arc, muted) + ctx.beginPath() + ctx.arc(cx, cy, root.arcRadius, startRad, startRad + totalRad) + ctx.strokeStyle = root.trackColor.toString() + ctx.lineWidth = root.arcWidth + ctx.lineCap = "round" + ctx.stroke() + + // Fill (proportional) + var f = Math.min(Math.max(root.fraction, 0.0), 1.0) + if (f > 0) { + ctx.beginPath() + ctx.arc(cx, cy, root.arcRadius, startRad, + startRad + totalRad * f) + ctx.strokeStyle = root.fillColor.toString() + ctx.lineWidth = root.arcWidth + ctx.lineCap = "round" + ctx.stroke() + } + } + } + + Item { + id: overlay + anchors.fill: parent + } +} diff --git a/src/qml/icons/DeleteIcon.qml b/src/qml/icons/DeleteIcon.qml new file mode 100644 index 0000000..04aa56e --- /dev/null +++ b/src/qml/icons/DeleteIcon.qml @@ -0,0 +1,18 @@ +import QtQuick + +// X pattern — delete / remove +// ● . . . ● +// . ● . ● . +// . . ● . . +// . ● . ● . +// ● . . . ● +// 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 + ] +} diff --git a/src/qml/icons/DotIcon.qml b/src/qml/icons/DotIcon.qml new file mode 100644 index 0000000..dc22a0c --- /dev/null +++ b/src/qml/icons/DotIcon.qml @@ -0,0 +1,74 @@ +import QtQuick + +// qmllint disable unqualified +Item { + id: root + + // Static pattern — flat array of 0/1, row-major + property var pattern: [] + + // Dimensions + property int columns: 5 + property int dotSize: 6 + property int dotSpacing: 4 + + // Appearance + property color dotColor: "white" + property real inactiveOpacity: 0.1 + property real activeOpacity: 0.9 + + // Animation + property bool animated: false + property int animPhase: 0 + + readonly property int rows: Math.max(1, Math.ceil(pattern.length / columns)) + readonly property int count: animated ? columns * columns : pattern.length + + implicitWidth: columns * dotSize + Math.max(0, columns - 1) * dotSpacing + implicitHeight: rows * dotSize + Math.max(0, rows - 1) * dotSpacing + width: implicitWidth + height: implicitHeight + + Timer { + interval: 140 + repeat: true + running: root.animated + onTriggered: root.animPhase = (root.animPhase + 1) % (root.columns * 2) + } + + Grid { + columns: root.columns + spacing: root.dotSpacing + + Repeater { + model: root.count + + Rectangle { + width: root.dotSize + height: root.dotSize + radius: root.dotSize * 0.3 + color: root.dotColor + + opacity: { + if (!root.animated) { + return (index < root.pattern.length + && root.pattern[index]) ? root.activeOpacity : root.inactiveOpacity + } + // Wave from center + const cx = Math.floor(root.columns / 2) + const cy = Math.floor(root.columns / 2) + const col = index % root.columns + const row = Math.floor(index / root.columns) + const d = Math.abs(col - cx) + Math.abs(row - cy) + const wave = root.animPhase % root.columns + const diff = Math.abs(d - wave) + if (diff === 0) + return root.activeOpacity + if (diff === 1) + return 0.35 + return root.inactiveOpacity + } + } + } + } +} diff --git a/src/qml/icons/DownloadIcon.qml b/src/qml/icons/DownloadIcon.qml new file mode 100644 index 0000000..558ad8b --- /dev/null +++ b/src/qml/icons/DownloadIcon.qml @@ -0,0 +1,17 @@ +import QtQuick + +// Downward arrow — download +// . . ● . . +// . . ● . . +// ● ● ● ● ● +// . ● ● ● . +// . . ● . . +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 1, 1, 1, 1, 1, + 0, 1, 1, 1, 0, + 0, 0, 1, 0, 0 + ] +} diff --git a/src/qml/icons/GuideIcon.qml b/src/qml/icons/GuideIcon.qml new file mode 100644 index 0000000..275c37a --- /dev/null +++ b/src/qml/icons/GuideIcon.qml @@ -0,0 +1,17 @@ +import QtQuick + +// Crosshair pattern — step-by-step guide +// . . ● . . +// . . ● . . +// ● ● . ● ● +// . . ● . . +// . . ● . . +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 1, 1, 0, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0 + ] +} diff --git a/src/qml/icons/NodeStatusIcon.qml b/src/qml/icons/NodeStatusIcon.qml new file mode 100644 index 0000000..4df6fab --- /dev/null +++ b/src/qml/icons/NodeStatusIcon.qml @@ -0,0 +1,72 @@ +import QtQuick +import Logos.Theme + +// qmllint disable unqualified +Item { + id: root + + property bool starting: true + property bool success: false + property int animPhase: 0 + + readonly property int columns: 7 + readonly property int dotSize: 8 + readonly property int dotSpacing: 5 + + implicitWidth: columns * dotSize + (columns - 1) * dotSpacing + implicitHeight: columns * dotSize + (columns - 1) * dotSpacing + width: implicitWidth + height: implicitHeight + + Timer { + interval: 120 + repeat: true + running: root.starting + onTriggered: root.animPhase = (root.animPhase + 1) % 14 + } + + Grid { + columns: root.columns + spacing: root.dotSpacing + + Repeater { + model: root.columns * root.columns + + Rectangle { + width: root.dotSize + height: root.dotSize + radius: root.dotSize * 0.25 + + color: { + if (root.success) + return Theme.palette.success + if (!root.starting) + return Theme.palette.error + return Theme.palette.text + } + + opacity: { + const col = index % root.columns + const row = Math.floor(index / root.columns) + const d = Math.abs(col - 3) + Math.abs(row - 3) + + if (root.starting) { + const wave = root.animPhase % root.columns + const diff = Math.abs(d - wave) + if (diff === 0) + return 0.9 + if (diff === 1) + return 0.35 + return 0.1 + } + + if (root.success) + return 0.85 + + // Error — X pattern + return (col === row || col + row === 6) ? 0.9 : 0.1 + } + } + } + } +} diff --git a/src/qml/icons/PlayIcon.qml b/src/qml/icons/PlayIcon.qml new file mode 100644 index 0000000..af62992 --- /dev/null +++ b/src/qml/icons/PlayIcon.qml @@ -0,0 +1,17 @@ +import QtQuick + +// Right-pointing triangle — play / start +// ● . . . . +// ● ● . . . +// ● ● ● . . +// ● ● . . . +// ● . . . . +DotIcon { + pattern: [ + 1, 0, 0, 0, 0, + 1, 1, 0, 0, 0, + 1, 1, 1, 0, 0, + 1, 1, 0, 0, 0, + 1, 0, 0, 0, 0 + ] +} diff --git a/src/qml/icons/PortIcon.qml b/src/qml/icons/PortIcon.qml new file mode 100644 index 0000000..196b526 --- /dev/null +++ b/src/qml/icons/PortIcon.qml @@ -0,0 +1,17 @@ +import QtQuick + +// Right arrow pattern — manual port forwarding +// . . ● . . +// . . . ● . +// ● ● ● ● ● +// . . . ● . +// . . ● . . +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 0, + 0, 0, 1, 0, 0 + ] +} diff --git a/src/qml/icons/SettingsIcon.qml b/src/qml/icons/SettingsIcon.qml new file mode 100644 index 0000000..b976fce --- /dev/null +++ b/src/qml/icons/SettingsIcon.qml @@ -0,0 +1,11 @@ +import QtQuick + +// Gear / cog icon — 4 cardinal teeth + ring with center hole +// . . ● . . +// . ● ● ● . +// ● ● . ● ● +// . ● ● ● . +// . . ● . . +DotIcon { + pattern: [0, 0, 1, 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/icons/StopIcon.qml b/src/qml/icons/StopIcon.qml new file mode 100644 index 0000000..d34a081 --- /dev/null +++ b/src/qml/icons/StopIcon.qml @@ -0,0 +1,17 @@ +import QtQuick + +// Filled square — stop +// . . . . . +// . ● ● ● . +// . ● ● ● . +// . ● ● ● . +// . . . . . +DotIcon { + pattern: [ + 0, 0, 0, 0, 0, + 0, 1, 1, 1, 0, + 0, 1, 1, 1, 0, + 0, 1, 1, 1, 0, + 0, 0, 0, 0, 0 + ] +} diff --git a/src/qml/icons/StorageIcon.qml b/src/qml/icons/StorageIcon.qml new file mode 100644 index 0000000..e5eae8b --- /dev/null +++ b/src/qml/icons/StorageIcon.qml @@ -0,0 +1,19 @@ +import QtQuick + +// Ring / node pattern — used in the StorageView header +// . ● ● ● . +// ● . . . ● +// ● . ● . ● +// ● . . . ● +// . ● ● ● . +DotIcon { + pattern: [ + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0 + ] + dotSize: 7 + dotSpacing: 5 +} diff --git a/src/qml/icons/UploadIcon.qml b/src/qml/icons/UploadIcon.qml new file mode 100644 index 0000000..66375d8 --- /dev/null +++ b/src/qml/icons/UploadIcon.qml @@ -0,0 +1,17 @@ +import QtQuick + +// Upward arrow — upload +// . . ● . . +// . ● ● ● . +// ● ● ● ● ● +// . . ● . . +// . . ● . . +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 1, 1, 1, 0, + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0 + ] +} diff --git a/src/qml/icons/UpnpIcon.qml b/src/qml/icons/UpnpIcon.qml new file mode 100644 index 0000000..6aa2c72 --- /dev/null +++ b/src/qml/icons/UpnpIcon.qml @@ -0,0 +1,17 @@ +import QtQuick + +// Diamond / network pattern — UPnP automatic port forwarding +// . . ● . . +// . ● . ● . +// ● . ● . ● +// . ● . ● . +// . . ● . . +DotIcon { + pattern: [ + 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0 + ] +} diff --git a/vendor/logos-design-system b/vendor/logos-design-system new file mode 160000 index 0000000..596811c --- /dev/null +++ b/vendor/logos-design-system @@ -0,0 +1 @@ +Subproject commit 596811cbb0a0644322267368e87fab80e34203d8 From e8c524dc41634ba870149b5bf05ad35c7ebb14ed Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 22 Feb 2026 08:48:08 +0400 Subject: [PATCH 07/19] Update paths --- src/storage_resources.qrc | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index 1ad838e..d1fe4c4 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -12,12 +12,29 @@ qml/HealthIndicator.qml qml/ModeSelector.qml qml/AdvancedSetup.qml - qml/DotIcon.qml - qml/NodeStatusIcon.qml - qml/GuideIcon.qml - qml/AdvancedIcon.qml - qml/UpnpIcon.qml - qml/PortIcon.qml + qml/ManifestTable.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 + qml/icons/AdvancedIcon.qml + qml/icons/UpnpIcon.qml + qml/icons/PortIcon.qml + qml/icons/StorageIcon.qml + qml/icons/PlayIcon.qml + qml/icons/StopIcon.qml + qml/icons/SettingsIcon.qml + qml/icons/UploadIcon.qml + qml/icons/DownloadIcon.qml + qml/icons/DeleteIcon.qml + qml/icons/ArcWidget.qml + icons/storage.png From 9b85c68aaf4e1b5aa58d409d52af1fa8d436be4f Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 22 Feb 2026 16:26:21 +0400 Subject: [PATCH 08/19] Add components --- src/qml/DebugPanel.qml | 91 ++++++++++++++++++++++ src/qml/NodeHeader.qml | 147 ++++++++++++++++++++++++++++++++++++ src/qml/StatusWidgets.qml | 155 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 393 insertions(+) create mode 100644 src/qml/DebugPanel.qml create mode 100644 src/qml/NodeHeader.qml create mode 100644 src/qml/StatusWidgets.qml diff --git a/src/qml/DebugPanel.qml b/src/qml/DebugPanel.qml new file mode 100644 index 0000000..19e0a59 --- /dev/null +++ b/src/qml/DebugPanel.qml @@ -0,0 +1,91 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +// qmllint disable unqualified +Rectangle { + id: root + + property var backend + property bool running: false + + color: Theme.palette.backgroundElevated + border.color: Theme.palette.borderSecondary + border.width: 1 + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 10 + Layout.topMargin: 6 + Layout.bottomMargin: 4 + spacing: 6 + + LogosStorageButton { + text: "Debug" + enabled: root.running + onClicked: root.backend.tryDebug() + } + LogosStorageButton { + text: "Peer ID" + enabled: root.running + onClicked: root.backend.showPeerId() + } + LogosStorageButton { + text: "Data dir" + enabled: root.running + onClicked: root.backend.dataDir() + } + LogosStorageButton { + text: "SPR" + enabled: root.running + onClicked: root.backend.spr() + } + LogosStorageButton { + text: "Version" + enabled: root.running + onClicked: root.backend.version() + } + + Item { + Layout.fillWidth: true + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Theme.palette.borderSecondary + } + + 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) + }) + } + } + } +} diff --git a/src/qml/NodeHeader.qml b/src/qml/NodeHeader.qml new file mode 100644 index 0000000..8f31498 --- /dev/null +++ b/src/qml/NodeHeader.qml @@ -0,0 +1,147 @@ +import QtQuick +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +// qmllint disable unqualified +RowLayout { + id: root + + property var backend + property bool nodeIsUp: false + property bool blinkOn: false + + 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 + + signal settingsRequested() + + 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.backend.status !== root.running) + return Theme.palette.textMuted + return root.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.backend.status !== root.running) + return Theme.palette.textMuted + return root.nodeIsUp ? Theme.palette.success : Theme.palette.error + } + opacity: root.backend.status === root.running + ? (root.blinkOn ? 1.0 : 0.15) : 1.0 + } + + LogosText { + text: { + switch (root.backend.status) { + case root.stopped: return "Stopped" + case root.starting: return "Starting…" + case root.running: return "Running" + case root.stopping: return "Stopping…" + case root.destroyed: return "Not initialised" + default: return "" + } + } + 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: root.settingsRequested() + } + } + + 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() + } + } +} diff --git a/src/qml/StatusWidgets.qml b/src/qml/StatusWidgets.qml new file mode 100644 index 0000000..604be4a --- /dev/null +++ b/src/qml/StatusWidgets.qml @@ -0,0 +1,155 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs +import QtQuick.Layouts +import Logos.Theme +import Logos.Controls + +// qmllint disable unqualified +ColumnLayout { + id: root + + property var backend + property bool running: false + + spacing: 0 + + TextEdit { + id: clipHelper + visible: false + function copyText(str) { + clipHelper.text = str + clipHelper.selectAll() + clipHelper.copy() + } + } + + FileDialog { + id: uploadDialog + onAccepted: root.backend.tryUploadFile(selectedFile) + } + + Connections { + target: root.backend + function onUploadCompleted(cid) { root._lastCid = cid } + function onDownloadCompleted(cid) { root._lastCid = cid } + } + + property string _lastCid: "" + + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacing.medium + + UploadWidget { + uploadProgress: root.backend.uploadProgress + running: root.running + onUploadRequested: uploadDialog.open() + } + + DiskWidget { + backend: root.backend + } + + PeersWidget { + backend: root.backend + } + + Item { + Layout.fillWidth: true + } + } + + Item { + Layout.fillWidth: true + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.preferredHeight: 36 + + opacity: root._lastCid.length > 0 ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: 200 + } + } + + Rectangle { + 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 = root._lastCid + 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 + } + } + + 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(root._lastCid) + copyFlashAnim.restart() + } + } + } + } +} From b25f14210ffcc49dcac45e9fe13b28edec459ac8 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 22 Feb 2026 16:37:27 +0400 Subject: [PATCH 09/19] 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 From 937c35100119f1bff39761de95558ae1b2291078 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 22 Feb 2026 17:25:01 +0400 Subject: [PATCH 10/19] Provide different fixes --- src/StorageBackend.cpp | 136 +++++-------------------------------- src/StorageBackend.h | 10 ++- src/qml/DebugPanel.qml | 2 +- src/qml/DiskWidget.qml | 2 +- src/qml/LogosTextField.qml | 2 - src/qml/ManifestTable.qml | 2 + src/qml/PortForwarding.qml | 1 + src/qml/SettingsPopup.qml | 1 - src/qml/StorageView.qml | 21 ++++-- 9 files changed, 49 insertions(+), 128 deletions(-) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index bd860b6..fdd87c7 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -79,11 +79,9 @@ LogosResult StorageBackend::init(const QString& configJson) { } else { setStatus(Running); debug("Storage module started."); - // QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); - QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); - QMetaObject::invokeMethod(this, &StorageBackend::tryDebug, Qt::QueuedConnection); - QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); + StorageBackend::fetchWidgetsData(); + emit startCompleted(); } })) { @@ -121,7 +119,6 @@ LogosResult StorageBackend::init(const QString& configJson) { m_uploadedBytes += len; - // Calcule le pourcentage if (m_uploadTotalBytes > 0) { m_uploadProgress = (m_uploadedBytes * 100) / m_uploadTotalBytes; } @@ -406,9 +403,6 @@ void StorageBackend::tryUploadFinalize() { void StorageBackend::tryUploadFile(const QUrl& url) { qDebug() << "StorageBackend: tryUploadFile called"; - qDebug() << " URL toString():" << url.toString(); - qDebug() << " URL toLocalFile():" << url.toLocalFile(); - qDebug() << " URL path():" << url.path(); if (!url.isLocalFile()) { qWarning() << "Not a local file"; @@ -426,33 +420,6 @@ void StorageBackend::tryUploadFile(const QUrl& url) { debug(QString("Starting upload of file: %1 bytes").arg(m_uploadTotalBytes)); - // QString filename = url.toLocalFile(); - - // // QString filename = "/home/arnaud/Work/logos/logos-storage-ui/README.md"; - // QString sessionId = m_logos->storage_module.uploadInit(filename); - - // qDebug() << "StorageBackend: uploadInit sessionId =" << sessionId; - - // QtConcurrent::run([this, url]() { - // LogosResult result = m_logos->storage_module.uploadUrl(url); - - // Go back to the main thread - // // Better to use signal - // QMetaObject::invokeMethod( - // this, - // [this, result]() { - // if (!result.success) { - // setStatus(m_status, result.getString()); - // return; - // } - - // QString sessionId = result.value.value(); - - // qDebug() << "StorageBackend: uploadFromPath result =" << sessionId; - // }, - // Qt::QueuedConnection); - // }); - // QTimer::singleShot(0, this, [this, url]() { LogosResult result = m_logos->storage_module.uploadUrl(url); if (!result.success) { @@ -461,83 +428,10 @@ void StorageBackend::tryUploadFile(const QUrl& url) { } QString sessionId = result.value.value(); - //}); + qDebug() << "StorageBackend: tryUploadFile result =" << sessionId; } -// void StorageBackend::tryUploadFile(const QUrl& url) { -// qDebug() << "StorageBackend:tryUploadFile called"; - -// if (!url.isLocalFile()) { -// qWarning() << "Not a local file"; -// m_statusText = "The provided URL is not a local file."; -// emit statusChanged(); -// return; -// } - -// QString localPath = url.toLocalFile(); -// qDebug() << " Uploading from:" << localPath; - -// QFile file(localPath); -// if (!file.open(QIODevice::ReadOnly)) { -// qWarning() << "Cannot open file for reading:" << localPath; -// m_statusText = "Cannot open file for reading: " + localPath; -// emit statusChanged(); -// return; -// } - -// const qint64 chunkSize = 1024 * 64; // 64KB -// qint64 totalSize = file.size(); -// qint64 bytesRead = 0; - -// QFileInfo fileInfo(localPath); -// QString filename = fileInfo.fileName(); - -// LogosResult result = m_logos->storage_module.uploadInit("test.txt", chunkSize); - -// if (!result.success) { -// debug(result.getString()); -// file.close(); -// return; -// } - -// QString sessionId = result.getString(); - -// while (!file.atEnd()) { -// QByteArray chunk = file.read(chunkSize); -// bytesRead += chunk.size(); - -// qDebug() << " Read chunk:" << chunk.size() << "bytes" -// << "Progress:" << bytesRead << "/" << totalSize; - -// result = m_logos->storage_module.uploadChunk(sessionId, chunk); - -// if (!result.success) { -// qWarning("StorageBackend:tryUploadFile failed to send uploadChunk command"); -// file.close(); -// return; -// } - -// // Calculate progress percentage -// int progress = (bytesRead * 100) / totalSize; -// qDebug() << " Progress:" << progress << "%"; -// } - -// file.close(); - -// result = m_logos->storage_module.uploadFinalize(sessionId); - -// if (!result.success) { -// qWarning("StorageBackend:tryUploadFile failed to send uploadFinalize command"); -// file.close(); -// return; -// } - -// qDebug() << "Upload complete, CID:" << result.getString(); - -// file.close(); -// } - void StorageBackend::tryDownloadFile(const QString& cid, const QUrl& url) { qDebug() << "StorageBackend: tryDownloadFile called"; @@ -547,13 +441,6 @@ void StorageBackend::tryDownloadFile(const QString& cid, const QUrl& url) { return; } - // QString filename = url.toLocalFile(); - - // // QString filename = "/home/arnaud/Work/logos/logos-storage-ui/README.md"; - // QString sessionId = m_logos->storage_module.uploadInit(filename); - - // qDebug() << "StorageBackend: uploadInit sessionId =" << sessionId; - LogosResult result = m_logos->storage_module.downloadToUrl(cid, url, false); if (!result.success) { @@ -701,6 +588,7 @@ void StorageBackend::downloadManifest(const QString& cid) { manifest["mimetype"] = mimetype; manifest["datasetSize"] = datasetSize; manifest["blockSize"] = blockSize; + } void StorageBackend::downloadManifests() { @@ -713,8 +601,6 @@ void StorageBackend::downloadManifests() { return; } - qDebug() << "StorageBackend::downloadManifests called, size=" << result.getList().size(); - emit manifestsUpdated(result.getList()); } @@ -818,6 +704,14 @@ void StorageBackend::saveUserConfig(const QString& configJson) { } else { debug("Failed to save config to " + USER_CONFIG_PATH); } + + QJsonDocument config = QJsonDocument::fromJson(configJson.toUtf8()); + if (config.isNull()) { + qDebug() << "StorageBackend::saveUserConfig invalid json config" << configJson; + return; + } + + m_config = config; } QJsonDocument StorageBackend::defaultConfig() { @@ -977,6 +871,12 @@ void StorageBackend::checkNodeIsUp() { } } +void StorageBackend::fetchWidgetsData() { + QMetaObject::invokeMethod(this, &StorageBackend::tryDebug, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); +} + void StorageBackend::status(StorageStatus status) { m_status = status; } void StorageBackend::loadUserConfig() { diff --git a/src/StorageBackend.h b/src/StorageBackend.h index ca8afc8..b4958dd 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -11,7 +11,8 @@ static const int RET_OK = 0; static const int RET_PROGRESS = 3; -static const QString ECHO_PROVIDER = "https://echo.codex.storage/"; +// static const QString ECHO_PROVIDER = "https://echo.codex.storage/"; +static const QString ECHO_PROVIDER = "https://ipv4.icanhazip.com"; 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"; @@ -137,6 +138,8 @@ class StorageBackend : public QObject { // Emit nodeIsntUp(error) on failure void checkNodeIsUp(); + void fetchWidgetsData(); + signals: void ready(); void startCompleted(); @@ -176,9 +179,14 @@ class StorageBackend : public QObject { LogosModules* m_logos; StorageStatus m_status; QString m_debugLogs; + + // TODO: double check if we need all of this parameters + // We could just have the progress passed using event and + // the error using reportError int m_uploadProgress = 0; QString m_uploadStatus = ""; qint64 m_uploadTotalBytes = 0; qint64 m_uploadedBytes = 0; + QJsonDocument m_config; }; diff --git a/src/qml/DebugPanel.qml b/src/qml/DebugPanel.qml index 19e0a59..7e32878 100644 --- a/src/qml/DebugPanel.qml +++ b/src/qml/DebugPanel.qml @@ -68,7 +68,7 @@ Rectangle { Layout.fillHeight: true clip: true contentWidth: width - contentHeight: debugText.paintedHeight + contentHeight: debugText.implicitHeight TextEdit { id: debugText diff --git a/src/qml/DiskWidget.qml b/src/qml/DiskWidget.qml index be0eab7..5bb5446 100644 --- a/src/qml/DiskWidget.qml +++ b/src/qml/DiskWidget.qml @@ -59,7 +59,7 @@ ArcWidget { } LogosText { - text: "STORAGE" + text: root.total > 0 ? "/ " + root.formatBytes(root.total) : "STORAGE" font.pixelSize: 9 color: Theme.palette.textTertiary font.letterSpacing: 1.3 diff --git a/src/qml/LogosTextField.qml b/src/qml/LogosTextField.qml index 08ad920..68b101e 100644 --- a/src/qml/LogosTextField.qml +++ b/src/qml/LogosTextField.qml @@ -8,12 +8,10 @@ TextField { 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 diff --git a/src/qml/ManifestTable.qml b/src/qml/ManifestTable.qml index 02922bb..61c67ac 100644 --- a/src/qml/ManifestTable.qml +++ b/src/qml/ManifestTable.qml @@ -65,11 +65,13 @@ ColumnLayout { LogosTextField { id: cidInput Layout.fillWidth: true + height: getManifestBtn.implicitHeight placeholderText: "Enter CID to download manifest…" isValid: true } LogosStorageButton { + id: getManifestBtn text: "GET MANIFEST" enabled: root.running && cidInput.text.length > 0 onClicked: { diff --git a/src/qml/PortForwarding.qml b/src/qml/PortForwarding.qml index 26bc48b..d281e58 100644 --- a/src/qml/PortForwarding.qml +++ b/src/qml/PortForwarding.qml @@ -50,6 +50,7 @@ LogosStorageLayout { LogosTextField { Layout.fillWidth: true + height: 60 id: tcpPortTextField placeholderText: "Enter the TCP port" text: root.tcpPort diff --git a/src/qml/SettingsPopup.qml b/src/qml/SettingsPopup.qml index f359600..53c2acd 100644 --- a/src/qml/SettingsPopup.qml +++ b/src/qml/SettingsPopup.qml @@ -67,7 +67,6 @@ Popup { enabled: jsonEditor.isValid onClicked: { 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 eeb985c..45ef6a1 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -15,7 +15,13 @@ LogosStorageLayout { return backend.status === 2 // StorageBackend.Running } - Component.onCompleted: root.backend.start() + Component.onCompleted: function () { + if (isRunning()) { + root.backend.fetchWidgetsData() + } else { + root.backend.start() + } + } HealthIndicator { id: health @@ -35,8 +41,12 @@ LogosStorageLayout { signal uploadCompleted(string cid) signal downloadCompleted(string cid) - function start() { status = 2 } - function stop() { status = 0 } + function start() { + status = 2 + } + function stop() { + status = 0 + } function checkNodeIsUp() {} function tryUploadFile(f) {} function downloadManifest(c) {} @@ -49,7 +59,10 @@ LogosStorageLayout { function version() {} function saveUserConfig(j) {} function reloadIfChanged(j) {} - function configJson() { return "{}" } + function configJson() { + return "{}" + } + function fetchWidgetsData() {} } SettingsPopup { From 56ab04de64e9f33bdc86a573dcdb9175d75ecb02 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 22 Feb 2026 17:40:20 +0400 Subject: [PATCH 11/19] Cleanup --- src/StorageBackend.cpp | 82 +--------------------------------- src/StorageBackend.h | 4 -- src/qml/LogosStorageLayout.qml | 2 - src/qml/Main.qml | 3 ++ 4 files changed, 4 insertions(+), 87 deletions(-) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index fdd87c7..ebcda7c 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -229,7 +229,7 @@ LogosResult StorageBackend::start(const QString& newConfigJson) { setStatus(Starting); debug("Starting Storage module..."); - // TODO trach the start attempts in a file + // TODO trace the start attempts in a file auto result = m_logos->storage_module.start(); @@ -335,72 +335,6 @@ void StorageBackend::tryDebug() { emit peersUpdated(nodes.size()); } -void StorageBackend::tryPeerConnect(const QString& peerId) { - qDebug().noquote() << "StorageBackend: tryPeerConnect called with peerId=" << peerId; - - // LogosResult result2 = m_logos->storage_module.space(); - // QVariantMap space = result2.getValue(); - // int quotaMaxBytes = space["quotaMaxBytes"].toInt(); - // int quotaUsedBytes = space["quotaUsedBytes"].toInt(); - // int quotaReservedBytes = space["quotaReservedBytes"].toInt(); - - // int totalBlocks = result2.getValue("totalBlocks"); - - // debug("totalBlocks " + QString::number(totalBlocks)); - // debug("quotaMaxBytes " + QString::number(quotaMaxBytes)); - // debug("quotaUsedBytes " + QString::number(quotaUsedBytes)); - // debug("quotaReservedBytes " + QString::number(quotaReservedBytes)); - - // LogosResult result = m_logos->storage_module.dataDir(); - // QString myDataDir = result.getString(); - // qDebug() << "StorageBackend: tryPeerConnect dataDir=" << myDataDir; - - // QString peerId = m_logos->storage_module.peerId(); - - // if (peerId.isEmpty()) { - // qDebug() << "StorageBackend: Peer ID is empty."; - // return; - // } - auto result = m_logos->storage_module.connect(peerId, QStringList()); - - qDebug() << "StorageBackend: peerConnect result =" << result.value; - // auto result = m_logos->storage_module.debug(); - - // debug("Debug " + result.getString()); - // QString filename = "test.txt"; - // QString sessionId = m_logos->storage_module.uploadInit(filename); - - // qDebug() << "StorageBackend: uploadInit sessionId =" << sessionId; - - // bool result = m_logos->storage_module.uploadCancel(sessionId); - - // qDebug() << "StorageBackend: uploadCancel result =" << result; -} - -void StorageBackend::tryUpload() { - qDebug() << "StorageBackend: tryUpload called"; - - // QString filename = "test.txt"; - // m_sessionId = m_logos->storage_module.uploadInit(filename); - - // qDebug() << "StorageBackend: uploadInit sessionId =" << m_sessionId; - - // QByteArray chunk = "Sample data chunk for upload."; - // bool result = m_logos->storage_module.uploadChunk(m_sessionId, chunk); - - // qDebug() << "StorageBackend: uploadChunk result =" << result; -} - -void StorageBackend::tryUploadFinalize() { - qDebug() << "StorageBackend: tryFinalize called"; - - // m_cid = m_logos->storage_module.uploadFinalize(m_sessionId); - - // qDebug() << "StorageBackend: uploadFinalize result =" << m_cid; - - // emit cidChanged(); -} - void StorageBackend::tryUploadFile(const QUrl& url) { qDebug() << "StorageBackend: tryUploadFile called"; @@ -623,19 +557,6 @@ void StorageBackend::space() { 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; - - LogosResult result = m_logos->storage_module.updateLogLevel(logLevel); - - if (!result.success) { - debug("StorageBackend::updateLogLevel failed with error=" + result.getError()); - return; - } - - debug("Log level updated to " + logLevel); -} - StorageBackend::StorageStatus StorageBackend::status() const { return m_status; } int StorageBackend::uploadProgress() const { return m_uploadProgress; } @@ -750,7 +671,6 @@ void StorageBackend::enableNatExtConfig(int tcpPort) { obj["listen-addrs"] = listenAddrs; // Fetch the public IP asynchronously so we can set nat=extip:IP in the config. - // If the request fails, we proceed without the IP (node will still start, just without extip NAT). debug("Retrieving public IP..."); QNetworkAccessManager* manager = new QNetworkAccessManager(this); diff --git a/src/StorageBackend.h b/src/StorageBackend.h index b4958dd..58d4087 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -70,10 +70,7 @@ class StorageBackend : public QObject { LogosResult start(const QString& configJson = ""); void destroy(); void stop(); - void tryPeerConnect(const QString& peerId); void tryDebug(); - void tryUpload(); - void tryUploadFinalize(); void exists(const QString& cid); void remove(const QString& cid); void fetch(const QString& cid); @@ -87,7 +84,6 @@ class StorageBackend : public QObject { void downloadManifests(); void space(); LogosResult init(const QString& configJson); - void updateLogLevel(const QString& logLevel); void status(StorageStatus status); // Save the user config passed in parameter diff --git a/src/qml/LogosStorageLayout.qml b/src/qml/LogosStorageLayout.qml index 0ca2ac8..7c22f0e 100644 --- a/src/qml/LogosStorageLayout.qml +++ b/src/qml/LogosStorageLayout.qml @@ -5,8 +5,6 @@ import Logos.Theme Rectangle { id: root color: Theme.palette.background - Layout.fillWidth: true - Layout.fillHeight: true implicitWidth: 600 implicitHeight: 400 } diff --git a/src/qml/Main.qml b/src/qml/Main.qml index e4a95f6..dabbf65 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QtCore import Logos.Theme @@ -22,6 +23,8 @@ Item { id: root implicitWidth: 800 implicitHeight: 800 + Layout.fillWidth: true + Layout.fillHeight: true property var backend: mockBackend From bf6c6bd06821ce9ef770c10f594c53d797c2c006 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 23 Feb 2026 11:15:19 +0400 Subject: [PATCH 12/19] Bunch of renaming and cleanup --- src/StorageBackend.cpp | 231 +++++++++++++++++++++----------------- src/StorageBackend.h | 116 ++++++++++++++++--- src/StorageUIPlugin.cpp | 27 +---- src/qml/DebugPanel.qml | 13 ++- src/qml/DiskWidget.qml | 4 +- src/qml/Main.qml | 2 - src/qml/ManifestTable.qml | 2 +- src/qml/StatusWidgets.qml | 21 ++-- src/qml/StorageView.qml | 14 +-- 9 files changed, 256 insertions(+), 174 deletions(-) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index ebcda7c..0f591d6 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -45,13 +45,35 @@ StorageBackend::~StorageBackend() m_logos = nullptr; } +void StorageBackend::reportError(const QString& message) { + debug(message, "warning"); + emit error(message); +} + +void StorageBackend::debug(const QString& log, const QString& level) { + if (!m_debugLogs.isEmpty()) { + m_debugLogs += "\n"; + } + + QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate); + m_debugLogs += timestamp + ": " + log; + + // Notify the property bind debugLogs + emit debugLogsChanged(); + + if (level == "warning") { + qWarning() << "StorageBackend: " << log; + } else { + qDebug() << "StorageBackend: " << log; + } +} + LogosResult StorageBackend::init(const QString& configJson) { qDebug() << "StorageBackend::initStorage called"; m_config = QJsonDocument::fromJson(configJson.toUtf8()); if (m_config.isNull()) { - qDebug() << "StorageBackend::initStorage invalid json config" << configJson; - reportError("Failed to create the storage: invalid JSON config"); + reportError("Failed to create the storage: invalid JSON config:" + configJson); return {false, "", "Failed to create the storage, invalid json config"}; } @@ -73,15 +95,23 @@ LogosResult StorageBackend::init(const QString& configJson) { if (!success) { QString message = data[1].toString(); setStatus(Stopped); - debug("Failed to start Storage module:" + message); + + // Used in StartNode component to detect + // failure in the onboarding. emit startFailed(message); - reportError("Failed to start: " + message); + + reportError("Failed to start Storage Module: " + message); } else { setStatus(Running); + debug("Storage module started."); + // Fetch data to in order to come on the app + // with fresh data. StorageBackend::fetchWidgetsData(); + // Used in StartNode component to detect + // success in the onboarding. emit startCompleted(); } })) { @@ -92,14 +122,18 @@ LogosResult StorageBackend::init(const QString& configJson) { bool success = data[0].toBool(); if (!success) { - QString message = data[1].toString(); setStatus(Running); - debug("Failed to stop Storage module:" + message); + + QString message = data[1].toString(); + reportError("Failed to stop Storage module:" + message); } else { setStatus(Stopped); + debug("Storage module stopped."); } + // Send stop completed signal event on failure + // to not block the shutdown process emit stopCompleted(); })) { qWarning() << "StorageWidget: failed to subscribe to storageStop events"; @@ -110,7 +144,8 @@ LogosResult StorageBackend::init(const QString& configJson) { if (!success) { QString message = data[1].toString(); - debug("Failure during upload progress: " + message); + reportError("Failure during upload progress: " + message); + m_uploadStatus = "Error: " + message; emit uploadStatusChanged(); } else { @@ -150,13 +185,12 @@ LogosResult StorageBackend::init(const QString& configJson) { 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::refreshSpace, Qt::QueuedConnection); QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); } })) { @@ -168,11 +202,12 @@ LogosResult StorageBackend::init(const QString& configJson) { if (!success) { QString message = data[1].toString(); - debug("Failure during download progress: " + message); + reportError("Failure during download progress: " + message); } else { QString sessionId = data[1].toString(); int len = data[2].toInt(); debug("Downloaded " + QString::number(len) + " bytes for session " + sessionId); + // TODO display progress here } })) { qWarning() << "StorageWidget: failed to subscribe to storageDownloadProgress events"; @@ -183,20 +218,24 @@ LogosResult StorageBackend::init(const QString& configJson) { if (!success) { QString message = data[1].toString(); - debug("Failed to download: " + message); + reportError("Failed to download: " + message); } else { QString sessionId = data[1].toString(); QString cid = data[2].toString(); - emit downloadCompleted(data[2].toString()); - QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); + // Notify the QML to display something (TBU) on download completed + emit downloadCompleted(cid); + + // Call refreshSpace to fetch the new space used by the node + // and reflect on the widget. + QMetaObject::invokeMethod(this, &StorageBackend::refreshSpace, Qt::QueuedConnection); + debug("Download completed for session " + sessionId + " with CID " + cid); } })) { qWarning() << "StorageWidget: failed to subscribe to storageDownloadProgress events"; } - emit initCompleted(); debug("new config is: " + configJson); return {true, ""}; @@ -235,7 +274,7 @@ LogosResult StorageBackend::start(const QString& newConfigJson) { if (!result) { setStatus(Stopped); - debug("Failed to start storage"); + reportError("Failed to start storage"); return {false, "", "Failed to start storage"}; } @@ -249,7 +288,6 @@ void StorageBackend::stop() { if (m_status == StorageStatus::Stopping) { debug("The Storage Module is already stopping."); - emit stopCompleted(); return; } @@ -266,7 +304,7 @@ void StorageBackend::stop() { if (!result.success) { setStatus(Running); - debug(result.getError()); + reportError("Error when trying to stop: " + result.getError()); return; } @@ -276,37 +314,17 @@ void StorageBackend::stop() { void StorageBackend::destroy() { qDebug() << "StorageBackend: destroy method called"; - StorageStatus status = m_status; auto result = m_logos->storage_module.destroy(); if (!result.success) { - debug(result.getError()); + reportError("Error when trying to destroy: " + result.getError()); return; } qDebug() << "StorageBackend: Storage module destroyed."; } -QString StorageBackend::debugLogs() const { return m_debugLogs; }; - -void StorageBackend::reportError(const QString& message) { - debug(message); - emit error(message); -} - -void StorageBackend::debug(const QString& log) { - if (!m_debugLogs.isEmpty()) { - m_debugLogs += "\n"; - } - - QString timestamp = QDateTime::currentDateTime().toString(Qt::ISODate); - m_debugLogs += timestamp + ": " + log; - emit debugLogsChanged(); - - qDebug() << "StorageBackend: " << log; -} - -void StorageBackend::tryDebug() { +void StorageBackend::logDebugInfo() { auto result = m_logos->storage_module.debug(); debug("Peer ID: " + result.getString("id")); @@ -335,12 +353,11 @@ void StorageBackend::tryDebug() { emit peersUpdated(nodes.size()); } -void StorageBackend::tryUploadFile(const QUrl& url) { - qDebug() << "StorageBackend: tryUploadFile called"; +void StorageBackend::uploadFile(const QUrl& url) { + qDebug() << "StorageBackend: uploadFile called"; if (!url.isLocalFile()) { - qWarning() << "Not a local file"; - debug("The provided URL is not a local file."); + reportError("The provided URL is not a local file."); return; } @@ -349,6 +366,7 @@ void StorageBackend::tryUploadFile(const QUrl& url) { m_uploadedBytes = 0; m_uploadTotalBytes = QFileInfo(url.toLocalFile()).size(); m_uploadStatus = "Starting upload..."; + emit uploadProgressChanged(); emit uploadStatusChanged(); @@ -357,34 +375,33 @@ void StorageBackend::tryUploadFile(const QUrl& url) { LogosResult result = m_logos->storage_module.uploadUrl(url); if (!result.success) { - debug(result.getError()); + reportError("Failed to upload file:" + result.getError()); return; } QString sessionId = result.value.value(); - qDebug() << "StorageBackend: tryUploadFile result =" << sessionId; + qDebug() << "StorageBackend: uploadFile result =" << sessionId; } -void StorageBackend::tryDownloadFile(const QString& cid, const QUrl& url) { - qDebug() << "StorageBackend: tryDownloadFile called"; +void StorageBackend::downloadFile(const QString& cid, const QUrl& url) { + qDebug() << "StorageBackend: downloadFile called"; if (!url.isLocalFile()) { - qWarning() << "Not a local file"; - debug("The provided URL is not a local file."); + reportError("The provided URL is not a local file."); return; } LogosResult result = m_logos->storage_module.downloadToUrl(cid, url, false); if (!result.success) { - debug(result.getError()); + reportError("Failed to download file:" + result.getError()); return; } QString sessionId = result.value.value(); - qDebug() << "StorageBackend: tryDownloadFile result =" << sessionId; + qDebug() << "StorageBackend: downloadFile result =" << sessionId; } void StorageBackend::exists(const QString& cid) { @@ -393,7 +410,7 @@ void StorageBackend::exists(const QString& cid) { LogosResult result = m_logos->storage_module.exists(cid); if (!result.success) { - debug("StorageBackend::exists failed with error=" + result.getError()); + reportError("StorageBackend::exists failed with error=" + result.getError()); return; } @@ -406,26 +423,25 @@ void StorageBackend::remove(const QString& cid) { LogosResult result = m_logos->storage_module.exists(cid); if (!result.success) { - debug("StorageBackend::remove failed to check exists: " + result.getError()); - emit error("Failed to check exists " + cid + ": " + result.getError()); + reportError("Failed to check exists: " + result.getError()); return; } if (!result.getBool()) { - debug("StorageBackend::remove blocks don't exist in store."); + debug("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()); + reportError("Failed to remove " + cid + ": " + result.getError()); return; } debug("Cid " + cid + " removed from local storage."); - QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); + // Refresh space data for Disk widget + QMetaObject::invokeMethod(this, &StorageBackend::refreshSpace, Qt::QueuedConnection); } void StorageBackend::fetch(const QString& cid) { @@ -434,59 +450,59 @@ void StorageBackend::fetch(const QString& cid) { LogosResult result = m_logos->storage_module.fetch(cid); if (!result.success) { - debug("StorageBackend::fetch failed with error=" + result.getError()); + reportError("Failed to fetch cid " + cid + ": " + result.getError()); return; } debug("Cid " + cid + " fetched."); } -void StorageBackend::version() { +void StorageBackend::logVersion() { qDebug() << "StorageBackend::version called"; LogosResult result = m_logos->storage_module.version(); if (!result.success) { - debug("StorageBackend::version failed with error=" + result.getError()); + reportError("Failed to log version: " + result.getError()); return; } debug("Version: " + result.getString()); } -void StorageBackend::showPeerId() { +void StorageBackend::logPeerId() { qDebug() << "StorageBackend::peerId called"; LogosResult result = m_logos->storage_module.peerId(); if (!result.success) { - debug("StorageBackend::peerId failed with error=" + result.getError()); + reportError("Failed to log peerId: " + result.getError()); return; } debug("Peer ID: " + result.getString()); } -void StorageBackend::spr() { +void StorageBackend::logSpr() { qDebug() << "StorageBackend::spr called"; LogosResult result = m_logos->storage_module.spr(); if (!result.success) { - debug("StorageBackend::spr failed with error=" + result.getError()); + reportError("Failed to log spr: " + result.getError()); return; } debug("SPR: " + result.getString()); } -void StorageBackend::dataDir() { +void StorageBackend::logDataDir() { qDebug() << "StorageBackend::dataDir called"; LogosResult result = m_logos->storage_module.dataDir(); if (!result.success) { - debug("StorageBackend::dataDir failed with error=" + result.getError()); + reportError("Failed to log dataDir: " + result.getError()); return; } @@ -499,7 +515,7 @@ void StorageBackend::downloadManifest(const QString& cid) { LogosResult result = m_logos->storage_module.downloadManifest(cid); if (!result.success) { - debug("StorageBackend::downloadManifest failed with error=" + result.getError()); + reportError("Failed to download manifest cid " + cid + ": " + result.getError()); return; } @@ -509,12 +525,6 @@ void StorageBackend::downloadManifest(const QString& cid) { 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); - QVariantMap manifest; manifest["cid"] = cid; manifest["treeCid"] = treeCid; @@ -523,6 +533,8 @@ void StorageBackend::downloadManifest(const QString& cid) { manifest["datasetSize"] = datasetSize; manifest["blockSize"] = blockSize; + // Refresh the table + downloadManifests(); } void StorageBackend::downloadManifests() { @@ -531,20 +543,20 @@ void StorageBackend::downloadManifests() { LogosResult result = m_logos->storage_module.manifests(); if (!result.success) { - debug("StorageBackend::downloadManifests failed with error=" + result.getError()); + reportError("Failed to download manifests: " + result.getError()); return; } emit manifestsUpdated(result.getList()); } -void StorageBackend::space() { - qDebug() << "StorageBackend::space called"; +void StorageBackend::refreshSpace() { + qDebug() << "StorageBackend::refreshSpace called"; LogosResult result = m_logos->storage_module.space(); if (!result.success) { - debug("Space failed with error=" + result.getError()); + reportError("Failed to refresh space: " + result.getError()); return; } @@ -555,14 +567,6 @@ void StorageBackend::space() { emit spaceUpdated(total, used); } -QString StorageBackend::configJson() const { return QString::fromUtf8(m_config.toJson(QJsonDocument::Indented)); } - -StorageBackend::StorageStatus StorageBackend::status() const { return m_status; } - -int StorageBackend::uploadProgress() const { return m_uploadProgress; } - -QString StorageBackend::uploadStatus() const { return m_uploadStatus; } - void StorageBackend::reloadIfChanged(const QString& configJson) { QJsonDocument config = QJsonDocument::fromJson(configJson.toUtf8()); if (config.isNull()) { @@ -587,7 +591,7 @@ void StorageBackend::reloadIfChanged(const QString& configJson) { LogosResult result = m_logos->storage_module.destroy(); if (!result.success) { - debug("Failed to destroy the context error=" + result.getError()); + reportError("Failed to destroy the context error=" + result.getError()); return; } else { setStatus(StorageStatus::Destroyed); @@ -597,7 +601,7 @@ void StorageBackend::reloadIfChanged(const QString& configJson) { LogosResult result = init(configJson); if (!result.success) { - debug("Failed to init context with new config: " + result.getError()); + reportError("Failed to init context with new config: " + result.getError()); return; } @@ -623,12 +627,12 @@ void StorageBackend::saveUserConfig(const QString& configJson) { file.close(); debug("Config saved to " + USER_CONFIG_PATH); } else { - debug("Failed to save config to " + USER_CONFIG_PATH); + reportError("Failed to save config to " + USER_CONFIG_PATH); } QJsonDocument config = QJsonDocument::fromJson(configJson.toUtf8()); if (config.isNull()) { - qDebug() << "StorageBackend::saveUserConfig invalid json config" << configJson; + reportError("Invalid json config" + configJson); return; } @@ -661,6 +665,7 @@ void StorageBackend::enableUpnpConfig() { reloadIfChanged(QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Indented))); } +// Note that debug is not used because the debug panel is not accessible yet void StorageBackend::enableNatExtConfig(int tcpPort) { qDebug() << "StorageBackend::enableNatExtConfig called with tcpPort" << tcpPort; @@ -671,7 +676,7 @@ void StorageBackend::enableNatExtConfig(int tcpPort) { obj["listen-addrs"] = listenAddrs; // Fetch the public IP asynchronously so we can set nat=extip:IP in the config. - debug("Retrieving public IP..."); + qDebug() << "StorageBackend:: Retrieving public IP..."; QNetworkAccessManager* manager = new QNetworkAccessManager(this); QNetworkRequest request(ECHO_PROVIDER); @@ -686,7 +691,7 @@ void StorageBackend::enableNatExtConfig(int tcpPort) { manager->deleteLater(); if (reply->error() != QNetworkReply::NoError) { - debug("Failed to retrieve public IP: " + reply->errorString() + ". Proceeding without extip NAT."); + qWarning() << "Failed to retrieve public IP: " << reply->errorString() << ". Proceeding without extip NAT."; } else { QString ip = QString::fromUtf8(reply->readAll()).trimmed(); debug("Public IP: " + ip); @@ -698,6 +703,7 @@ void StorageBackend::enableNatExtConfig(int tcpPort) { }); } +// Note that debug is not used because the debug panel is not accessible yet void StorageBackend::checkNodeIsUp() { qDebug() << "StorageBackend::checkNodeIsUp called."; @@ -705,7 +711,7 @@ void StorageBackend::checkNodeIsUp() { // the announceAddresses LogosResult result = m_logos->storage_module.debug(); if (!result.success) { - qDebug() << "Failed to get node debug info: " << result.getError(); + qWarning() << "StorageBackend::checkNodeIsUp Failed to get node debug info: " << result.getError(); emit nodeIsntUp("Failed to get node debug info: " + result.getError()); return; } @@ -715,13 +721,15 @@ void StorageBackend::checkNodeIsUp() { QVariantList nodes = table["nodes"].toList(); debug(QString("Connected peers: %1").arg(nodes.size())); + if (nodes.isEmpty()) { + qWarning() << "StorageBackend::checkNodeIsUp Not peers connected"; emit nodeIsntUp("No peers connected. " "Try modifying the discovery port (default 8090) in the advanced settings."); return; } - debug("DHT seems okay, found peers"); + qDebug() << "StorageBackend::checkNodeIsUp DHT seems okay, found peers"; // Extract IP+port pairs from announceAddresses. // Format: "/ip4/1.2.3.4/tcp/PORT" @@ -733,7 +741,7 @@ void StorageBackend::checkNodeIsUp() { 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(); + const int port = parts[tcpIndex + 1].toInt(); if (port > 0 && !ip.isEmpty()) endpoints.append({ ip, port }); } @@ -742,7 +750,8 @@ void StorageBackend::checkNodeIsUp() { QString nat = m_config.object()["nat"].toString(); if (endpoints.isEmpty()) { - debug("No TCP endpoints found in announce addresses"); + qDebug() << "StorageBackend::checkNodeIsUp 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."); @@ -753,7 +762,7 @@ void StorageBackend::checkNodeIsUp() { return; } - debug(QString("Checking reachability for %1 endpoint(s)...").arg(endpoints.size())); + qDebug() << "Checking reachability for " << endpoints.size() << "endpoint(s)..."; bool foundReachable = false; for (const auto& [ip, port] : endpoints) { @@ -767,12 +776,16 @@ void StorageBackend::checkNodeIsUp() { if (reply->error() == QNetworkReply::NoError) { const bool reachable = reply->readAll() == "True"; - debug(QString("%1:%2 is %3").arg(ip).arg(port).arg(reachable ? "reachable" : "not reachable")); + + QString status = reachable ? "reachable" : "not reachable"; + qDebug() << "StorageBackend::checkNodeIsUp " << ip << ":" << port << status; + if (reachable) { foundReachable = true; } } else { - debug(QString("Port check failed for %1:%2 : %3").arg(ip).arg(port).arg(reply->errorString())); + qDebug() << "StorageBackend::checkNodeIsUp Port check failed for" << ip << ":" << port + << reply->errorString(); } reply->deleteLater(); @@ -792,13 +805,11 @@ void StorageBackend::checkNodeIsUp() { } void StorageBackend::fetchWidgetsData() { - QMetaObject::invokeMethod(this, &StorageBackend::tryDebug, Qt::QueuedConnection); - QMetaObject::invokeMethod(this, &StorageBackend::space, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &StorageBackend::logDebugInfo, Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &StorageBackend::refreshSpace, Qt::QueuedConnection); QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); } -void StorageBackend::status(StorageStatus status) { m_status = status; } - void StorageBackend::loadUserConfig() { qDebug() << "StorageBackend::loadUserConfig called."; @@ -808,13 +819,23 @@ void StorageBackend::loadUserConfig() { if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) { result = init(QString::fromUtf8(file.readAll())); } else { - qWarning() << "StorageBackend::loadUserConfig Failed to read the user config file, fallback to default config"; + debug("Failed to read the user config file, fallback to default config"); result = init(QString::fromUtf8(defaultConfig().toJson(QJsonDocument::Indented))); } if (!result.success) { - qWarning() << "StorageBackend::loadUserConfig Failed to load the user config: " + result.getError(); + reportError("Failed to load the user config: " + result.getError()); } else { debug("User config loaded successfully"); } } + +QString StorageBackend::debugLogs() const { return m_debugLogs; }; + +QString StorageBackend::configJson() const { return QString::fromUtf8(m_config.toJson(QJsonDocument::Indented)); } + +StorageBackend::StorageStatus StorageBackend::status() const { return m_status; } + +int StorageBackend::uploadProgress() const { return m_uploadProgress; } + +QString StorageBackend::uploadStatus() const { return m_uploadStatus; } diff --git a/src/StorageBackend.h b/src/StorageBackend.h index 58d4087..acd3356 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -35,7 +35,7 @@ class StorageBackend : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(QString debugLogs READ debugLogs NOTIFY debugLogsChanged) - Q_PROPERTY(StorageStatus status READ status WRITE status NOTIFY statusChanged) + Q_PROPERTY(StorageStatus status READ status NOTIFY statusChanged) Q_PROPERTY(int uploadProgress READ uploadProgress NOTIFY uploadProgressChanged) Q_PROPERTY(QString uploadStatus READ uploadStatus NOTIFY uploadStatusChanged) public: @@ -61,30 +61,71 @@ class StorageBackend : public QObject { QString uploadStatus() const; Q_INVOKABLE QString configJson() const; + // Provide a default config for onboarding static QJsonDocument defaultConfig(); explicit StorageBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr); ~StorageBackend(); public slots: + // Init the Storage Module using the config json + // passed in parameter. + // It subscribes to events: + // 1- storageStart + // 2- storageStop + // 3- storageUploadProgress + // 4- storageUploadDone + // 5- storageDownloadProgress + // 6- storageDownloadProgress + LogosResult init(const QString& configJson); + + // Start the node + // If a configuration is passed (not empty string), + // the configuration will be reloaded before trying + // to start. LogosResult start(const QString& configJson = ""); + + // Destroy the Storage Module void destroy(); + + // Emit stopCompleted() on completion of it the module is not started void stop(); - void tryDebug(); + + // Log debug info + // Emit peersUpdated(int peers) + void logDebugInfo(); + + // Other log methods for debug + void logDataDir(); + void logVersion(); + void logSpr(); + void logPeerId(); + void exists(const QString& cid); void remove(const QString& cid); + + // Fetch a cid in background void fetch(const QString& cid); - void tryUploadFile(const QUrl& url); - void tryDownloadFile(const QString& cid, const QUrl& url); - void dataDir(); - void version(); - void spr(); - void showPeerId(); + + // Upload a file from the url + // Emit uploadProgressChanged and uploadStatusChanged on storageUploadProgress + // Emit uploadProgressChanged, uploadStatusChanged and uploadCompleted(cid) on storageUploadDone + void uploadFile(const QUrl& url); + + // Upload a file from the url + // Emit downloadCompleted(cid) on storageDownloadDone + void downloadFile(const QString& cid, const QUrl& url); + + // Emit manifestsUpdated void downloadManifest(const QString& cid); + + // Download all the manifests and notify + // Emit manifestsUpdated void downloadManifests(); - void space(); - LogosResult init(const QString& configJson); - void status(StorageStatus status); + + // Call space from the Storage Module + // Emit spaceUpdated to refresh the widget + void refreshSpace(); // Save the user config passed in parameter // into the user config json. @@ -108,8 +149,6 @@ class StorageBackend : public QObject { // // On success, the status will be set to Stopped. // - // Emit initCompleted on success. - // Emit initFailed on failure. void reloadIfChanged(const QString& configJson); // Enables the upnp in the config @@ -118,6 +157,7 @@ class StorageBackend : public QObject { // Enables the net external in the config // and re-create a context with the new configuration + // Emit natExtConfigCompleted void enableNatExtConfig(int tcpPort); // This method will ensure that the node is ready to be used. @@ -134,25 +174,48 @@ class StorageBackend : public QObject { // Emit nodeIsntUp(error) on failure void checkNodeIsUp(); + // Fetch multiple data for the widgets: manifests, debug.. void fetchWidgetsData(); signals: + // Used to start the Storage Module + // if the onboarding is already done void ready(); + + // Used in StartNode component to detect + // success in the onboarding. void startCompleted(); + + // Used in StartNode component to detect + // failure in the onboarding. void startFailed(const QString& error); + + // Refresh the node state indicator void statusChanged(); + + // Refresh the debug logs panel. void debugLogsChanged(); + + // Used in the shutdown process void stopCompleted(); + + // Used to refresh the disk widgets + void spaceUpdated(qlonglong total, qlonglong used); + void uploadProgressChanged(); void uploadStatusChanged(); + + // Used to refresh the Manifests table void manifestsUpdated(const QVariantList& manifests); - void quotaChanged(); - void initCompleted(); + + // Used in the on boarding to detect success void natExtConfigCompleted(); + void uploadCompleted(const QString& cid); void downloadCompleted(const QString& cid); + + // Display a toast message on error void error(const QString& message); - void spaceUpdated(qlonglong total, qlonglong used); // Emitted when the node port is reachable from the internet void nodeIsUp(); @@ -166,14 +229,29 @@ class StorageBackend : public QObject { private slots: private: + // Update the status + // Emit statusUpdated if the status was different from the previous status void setStatus(StorageStatus newStatus); - void peerConnect(const QString& peerId); - void debug(const QString& log); + + // Display debug (or message) in the terminal and + // add it to the debugLogs to make it accessible + // from the debug panel. + // Default level is debug, can be "warning" to display warning + // messages. + void debug(const QString& log, const QString& level = "debug"); + + // Display log and add it to debugLogs + // Emit error(message) void reportError(const QString& message); + // Logos related variables LogosAPI* m_logosAPI; LogosModules* m_logos; + + // Status of the Storage Module StorageStatus m_status; + + // List of debug logs displayed to the application. QString m_debugLogs; // TODO: double check if we need all of this parameters @@ -184,5 +262,7 @@ class StorageBackend : public QObject { qint64 m_uploadTotalBytes = 0; qint64 m_uploadedBytes = 0; + // Internal configuration object. It can be updated by + // upnp or port forwarning methods. QJsonDocument m_config; }; diff --git a/src/StorageUIPlugin.cpp b/src/StorageUIPlugin.cpp index 0513c92..22fecbe 100644 --- a/src/StorageUIPlugin.cpp +++ b/src/StorageUIPlugin.cpp @@ -26,19 +26,6 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) { qDebug() << "StorageUIPlugin: Loading settings..."; - // Default constructor uses QCoreApplication org/domain/app — same path as QML QtCore.Settings - // QSettings settings; - // int discoveryPort = settings.value("Storage/discoveryPort", 8090).toInt(); - // int tcpPort = settings.value("Storage/tcpPort", 0).toInt(); - // QString dataDir = settings.value("Storage/dataDir", "").toString(); - // bool onboardingCompleted = settings.value("Storage/onboardingCompleted", false).toBool(); - - // qDebug() << "StorageUIPlugin: Settings file:" << settings.fileName(); - // qDebug() << "StorageUIPlugin: onboardingCompleted=" << onboardingCompleted; - // qDebug() << "StorageUIPlugin: dataDir=" << dataDir; - // qDebug() << "StorageUIPlugin: discoveryPort=" << discoveryPort; - // qDebug() << "StorageUIPlugin: tcpPort=" << tcpPort; - // Always load Main.qml — QML handles navigation (onboarding vs startNode) StorageBackend* backend = new StorageBackend(logosAPI, quickWidget); @@ -54,18 +41,10 @@ QWidget* StorageUIPlugin::createWidget(LogosAPI* logosAPI) { root->setProperty("backend", QVariant::fromValue(static_cast(backend))); + // Here we emit an event ready because + // if the onboarding is already done, + // the backend can be init using onReady subscription backend->ready(); - // Storage init is done in the QML - // Build config from settings if onboarding was done, otherwise use empty config - // QString configJson = StorageBackend::getUserConfig(); - // qDebug() << "UserConfig" << StorageBackend::getUserConfigPath(); - // qDebug() << "configJson" << configJson; - - // LogosResult result = backend->init(configJson); - - // if (!result.success) { - // qWarning() << "StorageUIPlugin: Failed to init backend:" << result.getError(); - // } return quickWidget; } diff --git a/src/qml/DebugPanel.qml b/src/qml/DebugPanel.qml index 7e32878..2b3a1b6 100644 --- a/src/qml/DebugPanel.qml +++ b/src/qml/DebugPanel.qml @@ -28,27 +28,27 @@ Rectangle { LogosStorageButton { text: "Debug" enabled: root.running - onClicked: root.backend.tryDebug() + onClicked: root.backend.logDebugInfo() } LogosStorageButton { text: "Peer ID" enabled: root.running - onClicked: root.backend.showPeerId() + onClicked: root.backend.logPeerId() } LogosStorageButton { text: "Data dir" enabled: root.running - onClicked: root.backend.dataDir() + onClicked: root.backend.logDataDir() } LogosStorageButton { text: "SPR" enabled: root.running - onClicked: root.backend.spr() + onClicked: root.backend.logSpr() } LogosStorageButton { text: "Version" enabled: root.running - onClicked: root.backend.version() + onClicked: root.backend.logVersion() } Item { @@ -83,7 +83,8 @@ Rectangle { bottomPadding: 20 onTextChanged: Qt.callLater(function () { - logFlick.contentY = Math.max(0, logFlick.contentHeight - logFlick.height) + logFlick.contentY = Math.max( + 0, logFlick.contentHeight - logFlick.height) }) } } diff --git a/src/qml/DiskWidget.qml b/src/qml/DiskWidget.qml index 5bb5446..1d561f6 100644 --- a/src/qml/DiskWidget.qml +++ b/src/qml/DiskWidget.qml @@ -59,7 +59,8 @@ ArcWidget { } LogosText { - text: root.total > 0 ? "/ " + root.formatBytes(root.total) : "STORAGE" + text: root.total > 0 ? "/ " + root.formatBytes( + root.total) : "STORAGE" font.pixelSize: 9 color: Theme.palette.textTertiary font.letterSpacing: 1.3 @@ -72,7 +73,6 @@ ArcWidget { signal spaceUpdated(double total, double used) signal uploadCompleted - signal downloadCompleted function space() { return { diff --git a/src/qml/Main.qml b/src/qml/Main.qml index dabbf65..5d725ca 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -167,8 +167,6 @@ Item { signal startCompleted signal startFailed - signal stopCompleted - signal initCompleted signal ready signal error signal natExtConfigCompleted diff --git a/src/qml/ManifestTable.qml b/src/qml/ManifestTable.qml index 61c67ac..ee8d01b 100644 --- a/src/qml/ManifestTable.qml +++ b/src/qml/ManifestTable.qml @@ -24,7 +24,7 @@ ColumnLayout { fileMode: FileDialog.SaveFile onAccepted: { if (pendingManifest) { - root.backend.tryDownloadFile(pendingManifest.cid, selectedFile) + root.backend.downloadFile(pendingManifest.cid, selectedFile) pendingManifest = null } } diff --git a/src/qml/StatusWidgets.qml b/src/qml/StatusWidgets.qml index 604be4a..d863718 100644 --- a/src/qml/StatusWidgets.qml +++ b/src/qml/StatusWidgets.qml @@ -11,6 +11,7 @@ ColumnLayout { property var backend property bool running: false + property string _lastUploadedCid: "" spacing: 0 @@ -26,16 +27,17 @@ ColumnLayout { FileDialog { id: uploadDialog - onAccepted: root.backend.tryUploadFile(selectedFile) + onAccepted: root.backend.uploadFile(selectedFile) } Connections { target: root.backend - function onUploadCompleted(cid) { root._lastCid = cid } - function onDownloadCompleted(cid) { root._lastCid = cid } - } + function onUploadCompleted(cid) { + root._lastUploadedCid = cid + } - property string _lastCid: "" + function onDownloadCompleted() {} + } RowLayout { Layout.fillWidth: true @@ -66,7 +68,7 @@ ColumnLayout { Layout.bottomMargin: 10 Layout.preferredHeight: 36 - opacity: root._lastCid.length > 0 ? 1.0 : 0.0 + opacity: root._lastUploadedCid.length > 0 ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { @@ -95,7 +97,7 @@ ColumnLayout { LogosText { text: { - var c = root._lastCid + var c = root._lastUploadedCid return c.length > 20 ? c.substring(0, 8) + "…" + c.slice(-6) : c } font.pixelSize: 11 @@ -139,14 +141,15 @@ ColumnLayout { Rectangle { anchors.fill: parent radius: parent.radius - color: cidBadgeHover.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent" + color: cidBadgeHover.hovered ? Qt.rgba(1, 1, 1, + 0.04) : "transparent" } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { - clipHelper.copyText(root._lastCid) + clipHelper.copyText(root._lastUploadedCid) copyFlashAnim.restart() } } diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index 45ef6a1..bb24140 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -48,15 +48,15 @@ LogosStorageLayout { status = 0 } function checkNodeIsUp() {} - function tryUploadFile(f) {} + function uploadFile(f) {} function downloadManifest(c) {} function remove(c) {} - function tryDownloadFile(c, d) {} - function tryDebug() {} - function showPeerId() {} - function dataDir() {} - function spr() {} - function version() {} + function downloadFile(c, d) {} + function logDebugInfo() {} + function logPeerId() {} + function logDataDir() {} + function logSpr() {} + function logVersion() {} function saveUserConfig(j) {} function reloadIfChanged(j) {} function configJson() { From 78cbfd2be6077ca9e6b924e32b993a126e505b3c Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 23 Feb 2026 11:17:40 +0400 Subject: [PATCH 13/19] Rename widgets --- src/qml/CMakeLists.txt | 2 +- src/qml/StorageView.qml | 2 +- src/qml/{StatusWidgets.qml => Widgets.qml} | 0 src/storage_resources.qrc | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/qml/{StatusWidgets.qml => Widgets.qml} (100%) diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index 2e73910..d8dbc8a 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -159,7 +159,7 @@ qt_add_qml_module(appqml SettingsPopup.qml ManifestTable.qml NodeHeader.qml - StatusWidgets.qml + Widgets.qml DebugPanel.qml ) diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index bb24140..0739ba1 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -106,7 +106,7 @@ LogosStorageLayout { color: Theme.palette.borderSecondary } - StatusWidgets { + Widgets { Layout.fillWidth: true Layout.leftMargin: 24 Layout.rightMargin: 24 diff --git a/src/qml/StatusWidgets.qml b/src/qml/Widgets.qml similarity index 100% rename from src/qml/StatusWidgets.qml rename to src/qml/Widgets.qml diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index 43a6ab7..6bcdabd 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -14,7 +14,7 @@ qml/AdvancedSetup.qml qml/ManifestTable.qml qml/NodeHeader.qml - qml/StatusWidgets.qml + qml/StatusWidgets.qml qml/DebugPanel.qml qml/SettingsPopup.qml qml/JsonEditor.qml From 592abb3f848a39d838cf62ebf74116fea1a5faf6 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 23 Feb 2026 11:38:00 +0400 Subject: [PATCH 14/19] Move formatBytes into a util file --- src/qml/CMakeLists.txt | 1 + src/qml/DiskWidget.qml | 26 +++----------------------- src/qml/ManifestTable.qml | 14 ++------------ src/qml/UploadWidget.qml | 2 -- src/qml/Utils.js | 13 +++++++++++++ src/storage_resources.qrc | 1 + 6 files changed, 20 insertions(+), 37 deletions(-) create mode 100644 src/qml/Utils.js diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index d8dbc8a..7eda09a 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -161,6 +161,7 @@ qt_add_qml_module(appqml NodeHeader.qml Widgets.qml DebugPanel.qml + Utils.js ) # Set up QML module directory for runtime diff --git a/src/qml/DiskWidget.qml b/src/qml/DiskWidget.qml index 1d561f6..76fbb41 100644 --- a/src/qml/DiskWidget.qml +++ b/src/qml/DiskWidget.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import Logos.Theme import Logos.Controls +import "Utils.js" as Utils ArcWidget { id: root @@ -12,26 +13,6 @@ ArcWidget { 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" - } - - return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" - } - function refreshSpace() { let space = root.backend.space() root.total = space.total @@ -52,15 +33,14 @@ ArcWidget { spacing: 2 LogosText { - text: root.total > 0 ? root.formatBytes(root.used) : "—" + text: root.total > 0 ? Utils.formatBytes(root.used) : "—" font.pixelSize: 15 font.bold: true Layout.alignment: Qt.AlignHCenter } LogosText { - text: root.total > 0 ? "/ " + root.formatBytes( - root.total) : "STORAGE" + text: root.total > 0 ? "/ " + Utils.formatBytes(root.total) : "STORAGE" font.pixelSize: 9 color: Theme.palette.textTertiary font.letterSpacing: 1.3 diff --git a/src/qml/ManifestTable.qml b/src/qml/ManifestTable.qml index ee8d01b..14c2163 100644 --- a/src/qml/ManifestTable.qml +++ b/src/qml/ManifestTable.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import QtCore import Logos.Theme import Logos.Controls +import "Utils.js" as Utils // qmllint disable unqualified ColumnLayout { @@ -31,17 +32,6 @@ ColumnLayout { 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" - return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" - } Connections { target: root.backend @@ -189,7 +179,7 @@ ColumnLayout { } Text { width: 80 - text: root.formatBytes(parseInt(modelData.datasetSize)) + text: Utils.formatBytes(parseInt(modelData.datasetSize)) color: Theme.palette.textSecondary font.pixelSize: 11 elide: Text.ElideRight diff --git a/src/qml/UploadWidget.qml b/src/qml/UploadWidget.qml index 3df4ea3..3d5525d 100644 --- a/src/qml/UploadWidget.qml +++ b/src/qml/UploadWidget.qml @@ -18,12 +18,10 @@ ArcWidget { fraction: root.uploadProgress / 100.0 fillColor: root.isDone ? Theme.palette.success : Theme.palette.text - // ── Center content ──────────────────────────────────────────────────────── ColumnLayout { anchors.centerIn: parent spacing: 2 - // Idle or done: upload icon UploadIcon { dotColor: Theme.palette.textSecondary dotSize: 4 diff --git a/src/qml/Utils.js b/src/qml/Utils.js new file mode 100644 index 0000000..f461ee6 --- /dev/null +++ b/src/qml/Utils.js @@ -0,0 +1,13 @@ +.pragma library + +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" +} diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index 6bcdabd..419d4b1 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -35,6 +35,7 @@ qml/icons/DownloadIcon.qml qml/icons/DeleteIcon.qml qml/icons/ArcWidget.qml + qml/Utils.js icons/storage.png From c98dd29c67c82758e8804f219d7369a58aec5a9d Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 23 Feb 2026 11:40:23 +0400 Subject: [PATCH 15/19] Fix renaming --- src/storage_resources.qrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index 419d4b1..017cc8c 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -14,7 +14,7 @@ qml/AdvancedSetup.qml qml/ManifestTable.qml qml/NodeHeader.qml - qml/StatusWidgets.qml + qml/Widgets.qml qml/DebugPanel.qml qml/SettingsPopup.qml qml/JsonEditor.qml From 44929e119c8f7fc0371d13e4c5237916cd13ffbb Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 23 Feb 2026 11:57:23 +0400 Subject: [PATCH 16/19] Simplify the upload progress notifications --- src/StorageBackend.cpp | 46 +++++----------------------------------- src/StorageBackend.h | 24 +++++++-------------- src/qml/StorageView.qml | 4 ++-- src/qml/UploadWidget.qml | 30 ++++++++++++++++++++++++-- src/qml/Widgets.qml | 2 +- 5 files changed, 44 insertions(+), 62 deletions(-) diff --git a/src/StorageBackend.cpp b/src/StorageBackend.cpp index 0f591d6..8c3d2b1 100644 --- a/src/StorageBackend.cpp +++ b/src/StorageBackend.cpp @@ -145,26 +145,9 @@ LogosResult StorageBackend::init(const QString& configJson) { if (!success) { QString message = data[1].toString(); reportError("Failure during upload progress: " + message); - - m_uploadStatus = "Error: " + message; - emit uploadStatusChanged(); } else { - QString sessionId = data[1].toString(); qint64 len = data[2].toLongLong(); - - m_uploadedBytes += len; - - if (m_uploadTotalBytes > 0) { - m_uploadProgress = (m_uploadedBytes * 100) / m_uploadTotalBytes; - } - - m_uploadStatus = QString("Uploading: %1 / %2 bytes (%3%)") - .arg(m_uploadedBytes) - .arg(m_uploadTotalBytes) - .arg(m_uploadProgress); - - emit uploadProgressChanged(); - emit uploadStatusChanged(); + emit uploadChunk(len); } })) { qWarning() << "StorageWidget: failed to subscribe to storageUploadProgress events"; @@ -175,20 +158,11 @@ LogosResult StorageBackend::init(const QString& configJson) { if (!success) { QString message = data[1].toString(); - debug("Failed to upload: " + message); - m_uploadProgress = 0; - m_uploadStatus = "Upload failed"; - emit uploadProgressChanged(); - emit uploadStatusChanged(); + reportError("Failed to upload: " + message); } else { QString sessionId = data[1].toString(); QString cid = data[2].toString(); debug("Upload completed for session " + sessionId + " with CID " + cid); - - m_uploadProgress = 100; - m_uploadStatus = "Upload completed!"; - emit uploadProgressChanged(); - emit uploadStatusChanged(); emit uploadCompleted(cid); QMetaObject::invokeMethod(this, &StorageBackend::refreshSpace, Qt::QueuedConnection); QMetaObject::invokeMethod(this, &StorageBackend::downloadManifests, Qt::QueuedConnection); @@ -361,16 +335,9 @@ void StorageBackend::uploadFile(const QUrl& url) { return; } - // Reset and initialize progress tracking - m_uploadProgress = 0; - m_uploadedBytes = 0; - m_uploadTotalBytes = QFileInfo(url.toLocalFile()).size(); - m_uploadStatus = "Starting upload..."; - - emit uploadProgressChanged(); - emit uploadStatusChanged(); - - debug(QString("Starting upload of file: %1 bytes").arg(m_uploadTotalBytes)); + qint64 totalBytes = QFileInfo(url.toLocalFile()).size(); + debug(QString("Starting upload of file: %1 bytes").arg(totalBytes)); + emit uploadStarted(totalBytes); LogosResult result = m_logos->storage_module.uploadUrl(url); @@ -836,6 +803,3 @@ QString StorageBackend::configJson() const { return QString::fromUtf8(m_config.t StorageBackend::StorageStatus StorageBackend::status() const { return m_status; } -int StorageBackend::uploadProgress() const { return m_uploadProgress; } - -QString StorageBackend::uploadStatus() const { return m_uploadStatus; } diff --git a/src/StorageBackend.h b/src/StorageBackend.h index acd3356..3113aac 100644 --- a/src/StorageBackend.h +++ b/src/StorageBackend.h @@ -36,8 +36,6 @@ class StorageBackend : public QObject { QML_ELEMENT Q_PROPERTY(QString debugLogs READ debugLogs NOTIFY debugLogsChanged) Q_PROPERTY(StorageStatus status READ status NOTIFY statusChanged) - Q_PROPERTY(int uploadProgress READ uploadProgress NOTIFY uploadProgressChanged) - Q_PROPERTY(QString uploadStatus READ uploadStatus NOTIFY uploadStatusChanged) public: enum StorageStatus { // Stopped means that the context is created but the module is not started @@ -57,8 +55,6 @@ class StorageBackend : public QObject { QString debugLogs() const; StorageStatus status() const; - int uploadProgress() const; - QString uploadStatus() const; Q_INVOKABLE QString configJson() const; // Provide a default config for onboarding @@ -108,8 +104,9 @@ class StorageBackend : public QObject { void fetch(const QString& cid); // Upload a file from the url - // Emit uploadProgressChanged and uploadStatusChanged on storageUploadProgress - // Emit uploadProgressChanged, uploadStatusChanged and uploadCompleted(cid) on storageUploadDone + // Emit uploadStarted(totalBytes) when the upload begins + // Emit uploadChunk(len) on each storageUploadProgress event + // Emit uploadCompleted(cid) on storageUploadDone void uploadFile(const QUrl& url); // Upload a file from the url @@ -202,8 +199,11 @@ class StorageBackend : public QObject { // Used to refresh the disk widgets void spaceUpdated(qlonglong total, qlonglong used); - void uploadProgressChanged(); - void uploadStatusChanged(); + // Emitted when an upload starts, with the total file size + void uploadStarted(qint64 totalBytes); + + // Emitted for each chunk received during upload + void uploadChunk(qint64 len); // Used to refresh the Manifests table void manifestsUpdated(const QVariantList& manifests); @@ -254,14 +254,6 @@ class StorageBackend : public QObject { // List of debug logs displayed to the application. QString m_debugLogs; - // TODO: double check if we need all of this parameters - // We could just have the progress passed using event and - // the error using reportError - int m_uploadProgress = 0; - QString m_uploadStatus = ""; - qint64 m_uploadTotalBytes = 0; - qint64 m_uploadedBytes = 0; - // Internal configuration object. It can be updated by // upnp or port forwarning methods. QJsonDocument m_config; diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index 0739ba1..f9842db 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -32,12 +32,12 @@ LogosStorageLayout { id: mockBackend property var status: 0 property var debugLogs: "Hello!" - property string uploadStatus: "" - property int uploadProgress: 0 property var manifests: [] signal nodeIsUp signal nodeIsntUp(string reason) signal peersUpdated(int count) + signal uploadStarted(real totalBytes) + signal uploadChunk(real len) signal uploadCompleted(string cid) signal downloadCompleted(string cid) diff --git a/src/qml/UploadWidget.qml b/src/qml/UploadWidget.qml index 3d5525d..ce3f8f2 100644 --- a/src/qml/UploadWidget.qml +++ b/src/qml/UploadWidget.qml @@ -6,14 +6,40 @@ import Logos.Controls ArcWidget { id: root - property int uploadProgress: 0 // 0–100 + property var backend property bool running: false + property real totalBytes: 0 + property real uploadedBytes: 0 + + readonly property int uploadProgress: { + if (totalBytes <= 0) { + return 0 + } + return Math.min(Math.round(uploadedBytes / totalBytes * 100), 100) + } + + signal uploadRequested readonly property bool isUploading: uploadProgress > 0 && uploadProgress < 100 readonly property bool isDone: uploadProgress >= 100 - signal uploadRequested + Connections { + target: root.backend + + function onUploadStarted(totalBytes) { + root._totalBytes = totalBytes + root._uploadedBytes = 0 + } + + function onUploadChunk(len) { + root.uploadedBytes += len + } + + function onUploadCompleted(cid) { + root._uploadedBytes = root.totalBytes // force 100% + } + } fraction: root.uploadProgress / 100.0 fillColor: root.isDone ? Theme.palette.success : Theme.palette.text diff --git a/src/qml/Widgets.qml b/src/qml/Widgets.qml index d863718..6a0ff90 100644 --- a/src/qml/Widgets.qml +++ b/src/qml/Widgets.qml @@ -44,7 +44,7 @@ ColumnLayout { spacing: Theme.spacing.medium UploadWidget { - uploadProgress: root.backend.uploadProgress + backend: root.backend running: root.running onUploadRequested: uploadDialog.open() } From b287a4c1ee2df1277d5577f369c3aa8b28643ac4 Mon Sep 17 00:00:00 2001 From: E M <5089238+emizzle@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:23:48 +1100 Subject: [PATCH 17/19] add logos types from the sdk --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 791805a..33a23c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -176,6 +176,8 @@ if(_cpp_sdk_is_source) ${LOGOS_CPP_SDK_ROOT}/cpp/token_manager.h ${LOGOS_CPP_SDK_ROOT}/cpp/module_proxy.cpp ${LOGOS_CPP_SDK_ROOT}/cpp/module_proxy.h + ${LOGOS_CPP_SDK_ROOT}/cpp/logos_types.cpp + ${LOGOS_CPP_SDK_ROOT}/cpp/logos_types.h ) endif() From 49f3a3e147f5102852703e1b639cbb541b65e2c9 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 23 Feb 2026 12:32:45 +0400 Subject: [PATCH 18/19] Add mock file --- src/qml/MockBackend.qml | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/qml/MockBackend.qml diff --git a/src/qml/MockBackend.qml b/src/qml/MockBackend.qml new file mode 100644 index 0000000..d350a17 --- /dev/null +++ b/src/qml/MockBackend.qml @@ -0,0 +1,47 @@ +pragma Singleton +import QtQuick + +QtObject { + readonly property bool isMock: true + property int status: 0 + property string debugLogs: "Hello!" + + signal ready + signal startCompleted + signal startFailed(string error) + signal error(string message) + signal natExtConfigCompleted + signal nodeIsUp + signal nodeIsntUp(string reason) + signal peersUpdated(int count) + signal uploadStarted(real totalBytes) + signal uploadChunk(real len) + signal uploadCompleted(string cid) + signal downloadCompleted(string cid) + signal spaceUpdated(real total, real used) + signal manifestsUpdated(var manifests) + signal stopCompleted + + function start() { status = 2 } + function stop() { status = 0 } + function destroy() {} + function checkNodeIsUp() {} + function fetchWidgetsData() {} + function uploadFile(url) {} + function downloadFile(cid, url) {} + function downloadManifest(cid) {} + function downloadManifests() {} + function remove(cid) {} + function logDebugInfo() {} + function logPeerId() {} + function logDataDir() {} + function logSpr() {} + function logVersion() {} + function saveUserConfig(json) {} + function saveCurrentConfig() {} + function loadUserConfig() {} + function reloadIfChanged(json) {} + function enableUpnpConfig() {} + function enableNatExtConfig(tcpPort) { natExtConfigCompleted() } + function configJson() { return "{}" } +} From a6948988c6fde4736ebaf9629201b6c233b24633 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 23 Feb 2026 13:27:56 +0400 Subject: [PATCH 19/19] Use single mock instance --- CMakeLists.txt | 1 + src/qml/AdvancedSetup.qml | 2 +- src/qml/CMakeLists.txt | 3 +++ src/qml/DebugPanel.qml | 2 +- src/qml/DiskWidget.qml | 14 +------------ src/qml/HealthIndicator.qml | 2 +- src/qml/Main.qml | 42 +------------------------------------ src/qml/ManifestTable.qml | 2 +- src/qml/NodeHeader.qml | 2 +- src/qml/OnBoarding.qml | 7 +------ src/qml/PeersWidget.qml | 2 +- src/qml/PortForwarding.qml | 7 +------ src/qml/SettingsPopup.qml | 2 +- src/qml/StartNode.qml | 17 +-------------- src/qml/StorageView.qml | 39 +--------------------------------- src/qml/UploadWidget.qml | 8 +++---- src/qml/Widgets.qml | 2 +- src/storage_resources.qrc | 1 + 18 files changed, 23 insertions(+), 132 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 33a23c6..51b882b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) +set(LOGOS_STORAGE_UI_USE_VENDOR OFF) ########### DEPENDENCIES SECTION ########### diff --git a/src/qml/AdvancedSetup.qml b/src/qml/AdvancedSetup.qml index 502a8c1..4a284cd 100644 --- a/src/qml/AdvancedSetup.qml +++ b/src/qml/AdvancedSetup.qml @@ -7,7 +7,7 @@ import Logos.Controls LogosStorageLayout { id: root - property var backend: null + property var backend: MockBackend signal back signal completed diff --git a/src/qml/CMakeLists.txt b/src/qml/CMakeLists.txt index 7eda09a..776243d 100644 --- a/src/qml/CMakeLists.txt +++ b/src/qml/CMakeLists.txt @@ -119,6 +119,8 @@ target_link_libraries(appqml PRIVATE ) # Define the qml module and the StorageBackend sources. +set_source_files_properties(MockBackend.qml PROPERTIES QML_SINGLETON TRUE) + qt_add_qml_module(appqml URI StorageBackend VERSION 1.0 @@ -162,6 +164,7 @@ qt_add_qml_module(appqml Widgets.qml DebugPanel.qml Utils.js + MockBackend.qml ) # Set up QML module directory for runtime diff --git a/src/qml/DebugPanel.qml b/src/qml/DebugPanel.qml index 2b3a1b6..a09dc88 100644 --- a/src/qml/DebugPanel.qml +++ b/src/qml/DebugPanel.qml @@ -7,7 +7,7 @@ import Logos.Controls Rectangle { id: root - property var backend + property var backend: MockBackend property bool running: false color: Theme.palette.backgroundElevated diff --git a/src/qml/DiskWidget.qml b/src/qml/DiskWidget.qml index 76fbb41..7b818e3 100644 --- a/src/qml/DiskWidget.qml +++ b/src/qml/DiskWidget.qml @@ -7,7 +7,7 @@ import "Utils.js" as Utils ArcWidget { id: root - property var backend: mockBackend + property var backend: MockBackend property double total: 0 property double used: 0 @@ -48,17 +48,5 @@ ArcWidget { } } - QtObject { - id: mockBackend - signal spaceUpdated(double total, double used) - signal uploadCompleted - - function space() { - return { - "total": 0, - "used": 0 - } - } - } } diff --git a/src/qml/HealthIndicator.qml b/src/qml/HealthIndicator.qml index bdb5184..4f4a5d5 100644 --- a/src/qml/HealthIndicator.qml +++ b/src/qml/HealthIndicator.qml @@ -3,7 +3,7 @@ import QtQuick QtObject { id: root - property var backend + property var backend: MockBackend property bool nodeIsUp: false property bool blinkOn: true readonly property int threeMinutes: 180000 diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 5d725ca..f611da8 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -26,7 +26,7 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - property var backend: mockBackend + property var backend: MockBackend Connections { target: root.backend @@ -159,44 +159,4 @@ Item { anchors.bottomMargin: Theme.spacing.medium } - QtObject { - id: mockBackend - - readonly property bool isMock: true - property int status - - signal startCompleted - signal startFailed - signal ready - signal error - signal natExtConfigCompleted - signal nodeIsUp - signal nodeIsntUp - - function start() { - console.log("mock start called") - } - - function saveUserConfig() {} - - function loadUserConfig() {} - - function reloadIfChanged() {} - - function enableUpnpConfig() {} - - function enableNatExtConfig() { - natExtConfigCompleted() - } - - function saveCurrentConfig() {} - - function stop() {} - - function checkNodeIsUp() {} - - function guessResolution() { - return "" - } - } } diff --git a/src/qml/ManifestTable.qml b/src/qml/ManifestTable.qml index 14c2163..9369549 100644 --- a/src/qml/ManifestTable.qml +++ b/src/qml/ManifestTable.qml @@ -11,7 +11,7 @@ import "Utils.js" as Utils ColumnLayout { id: root - property var backend + property var backend: MockBackend property bool running: false property var manifests: [] diff --git a/src/qml/NodeHeader.qml b/src/qml/NodeHeader.qml index 8f31498..5ef18aa 100644 --- a/src/qml/NodeHeader.qml +++ b/src/qml/NodeHeader.qml @@ -7,7 +7,7 @@ import Logos.Controls RowLayout { id: root - property var backend + property var backend: MockBackend property bool nodeIsUp: false property bool blinkOn: false diff --git a/src/qml/OnBoarding.qml b/src/qml/OnBoarding.qml index 5a5dd60..1562a21 100644 --- a/src/qml/OnBoarding.qml +++ b/src/qml/OnBoarding.qml @@ -6,7 +6,7 @@ import Logos.Controls LogosStorageLayout { id: root - property var backend: mockBackend + property var backend: MockBackend signal back signal completed(bool upnpEnabled) @@ -156,9 +156,4 @@ LogosStorageLayout { } } - QtObject { - id: mockBackend - - function enableUpnpConfig() {} - } } diff --git a/src/qml/PeersWidget.qml b/src/qml/PeersWidget.qml index c2e1665..4903874 100644 --- a/src/qml/PeersWidget.qml +++ b/src/qml/PeersWidget.qml @@ -6,7 +6,7 @@ import Logos.Controls ArcWidget { id: root - property var backend + property var backend: MockBackend property int peers: 0 property int maxPeers: 20 diff --git a/src/qml/PortForwarding.qml b/src/qml/PortForwarding.qml index d281e58..d457a6b 100644 --- a/src/qml/PortForwarding.qml +++ b/src/qml/PortForwarding.qml @@ -8,7 +8,7 @@ LogosStorageLayout { property var tcpPort: 0 property bool loading: false - property var backend: mockBackend + property var backend: MockBackend signal back signal completed(int port) @@ -96,9 +96,4 @@ LogosStorageLayout { } } - QtObject { - id: mockBackend - - function enableNatExtConfig(port) {} - } } diff --git a/src/qml/SettingsPopup.qml b/src/qml/SettingsPopup.qml index 53c2acd..d4e7096 100644 --- a/src/qml/SettingsPopup.qml +++ b/src/qml/SettingsPopup.qml @@ -7,7 +7,7 @@ import Logos.Controls Popup { id: root - property var backend + property var backend: MockBackend modal: true width: 520 diff --git a/src/qml/StartNode.qml b/src/qml/StartNode.qml index 0e99a74..ad8b5f5 100644 --- a/src/qml/StartNode.qml +++ b/src/qml/StartNode.qml @@ -6,7 +6,7 @@ import Logos.Theme LogosStorageLayout { id: root - property var backend: mockBackend + property var backend: MockBackend property string status: "" property string title: "Starting your node" property string resolution: "" @@ -131,19 +131,4 @@ LogosStorageLayout { onTriggered: root.onNodeStarted() } - QtObject { - id: mockBackend - - readonly property bool isMock: true - - signal startCompleted - signal startFailed(string error) - signal nodeIsUp - signal nodeIsntUp(string reason) - - function checkNodeIsUp() {} - function stop() {} - function saveCurrentConfig() {} - function start() {} - } } diff --git a/src/qml/StorageView.qml b/src/qml/StorageView.qml index f9842db..7d7d1ff 100644 --- a/src/qml/StorageView.qml +++ b/src/qml/StorageView.qml @@ -8,7 +8,7 @@ import Logos.Controls LogosStorageLayout { id: root - property var backend: mockBackend + property var backend: MockBackend property bool showDebug: false function isRunning() { @@ -28,43 +28,6 @@ LogosStorageLayout { backend: root.backend } - QtObject { - id: mockBackend - property var status: 0 - property var debugLogs: "Hello!" - property var manifests: [] - signal nodeIsUp - signal nodeIsntUp(string reason) - signal peersUpdated(int count) - signal uploadStarted(real totalBytes) - signal uploadChunk(real len) - signal uploadCompleted(string cid) - signal downloadCompleted(string cid) - - function start() { - status = 2 - } - function stop() { - status = 0 - } - function checkNodeIsUp() {} - function uploadFile(f) {} - function downloadManifest(c) {} - function remove(c) {} - function downloadFile(c, d) {} - function logDebugInfo() {} - function logPeerId() {} - function logDataDir() {} - function logSpr() {} - function logVersion() {} - function saveUserConfig(j) {} - function reloadIfChanged(j) {} - function configJson() { - return "{}" - } - function fetchWidgetsData() {} - } - SettingsPopup { id: settingsPopup backend: root.backend diff --git a/src/qml/UploadWidget.qml b/src/qml/UploadWidget.qml index ce3f8f2..aafef56 100644 --- a/src/qml/UploadWidget.qml +++ b/src/qml/UploadWidget.qml @@ -6,7 +6,7 @@ import Logos.Controls ArcWidget { id: root - property var backend + property var backend: MockBackend property bool running: false property real totalBytes: 0 property real uploadedBytes: 0 @@ -28,8 +28,8 @@ ArcWidget { target: root.backend function onUploadStarted(totalBytes) { - root._totalBytes = totalBytes - root._uploadedBytes = 0 + root.totalBytes = totalBytes + root.uploadedBytes = 0 } function onUploadChunk(len) { @@ -37,7 +37,7 @@ ArcWidget { } function onUploadCompleted(cid) { - root._uploadedBytes = root.totalBytes // force 100% + root.uploadedBytes = root.totalBytes // force 100% } } diff --git a/src/qml/Widgets.qml b/src/qml/Widgets.qml index 6a0ff90..10a55eb 100644 --- a/src/qml/Widgets.qml +++ b/src/qml/Widgets.qml @@ -9,7 +9,7 @@ import Logos.Controls ColumnLayout { id: root - property var backend + property var backend: MockBackend property bool running: false property string _lastUploadedCid: "" diff --git a/src/storage_resources.qrc b/src/storage_resources.qrc index 017cc8c..50601c5 100644 --- a/src/storage_resources.qrc +++ b/src/storage_resources.qrc @@ -36,6 +36,7 @@ qml/icons/DeleteIcon.qml qml/icons/ArcWidget.qml qml/Utils.js + qml/MockBackend.qml icons/storage.png