diff --git a/src/qml/Main.qml b/src/qml/Main.qml
index 1074b7c..11c5800 100644
--- a/src/qml/Main.qml
+++ b/src/qml/Main.qml
@@ -20,7 +20,23 @@ Rectangle {
property int histIndex: -1
// --- Navigation API (pages call these via the injected `explorer`) ---
+ function sameDescriptor(a, b) {
+ if (!a || !b)
+ return false;
+ if (a.type !== b.type)
+ return false;
+ if (a.type === "home")
+ return true;
+ if (a.type === "search")
+ return a.query === b.query;
+ return a.param === b.param;
+ }
function navigateTo(desc) {
+ // Navigating to the page we're already on is a no-op — don't grow the
+ // history (so e.g. Home-while-home doesn't wake the Back button).
+ var current = root.histIndex >= 0 ? root.history[root.histIndex] : null;
+ if (root.sameDescriptor(current, desc))
+ return;
var h = root.history.slice(0, root.histIndex + 1);
h.push(desc);
root.history = h;
diff --git a/src/qml/components/CopyButton.qml b/src/qml/components/CopyButton.qml
new file mode 100644
index 0000000..7ba6d66
--- /dev/null
+++ b/src/qml/components/CopyButton.qml
@@ -0,0 +1,55 @@
+import QtQuick
+import Logos.Theme
+
+// Tiny copy-to-clipboard button. Self-contained: writes `value` to the system
+// clipboard via a hidden TextEdit (which runs in the host GUI process, so it
+// reaches the real desktop clipboard), then flips to a check mark briefly as
+// confirmation.
+Item {
+ id: root
+
+ property string value: ""
+ property int size: 22
+ implicitWidth: size
+ implicitHeight: size
+
+ // Off-screen helper used purely to push text onto the clipboard.
+ TextEdit { id: clip; visible: false }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.spacing.radiusSmall
+ color: hover.hovered ? Theme.palette.backgroundSecondary : "transparent"
+
+ HoverHandler { id: hover }
+ TapHandler {
+ onTapped: {
+ clip.text = root.value;
+ clip.selectAll();
+ clip.copy();
+ clip.text = "";
+ icon.source = icon.checkUrl;
+ resetTimer.restart();
+ }
+ }
+
+ Image {
+ id: icon
+ anchors.centerIn: parent
+ property url copyUrl: Qt.resolvedUrl("../icons/copy.svg")
+ property url checkUrl: Qt.resolvedUrl("../icons/check.svg")
+ source: copyUrl
+ sourceSize.width: Math.round(root.size * 0.72)
+ sourceSize.height: Math.round(root.size * 0.72)
+ width: Math.round(root.size * 0.72)
+ height: Math.round(root.size * 0.72)
+ fillMode: Image.PreserveAspectFit
+ }
+
+ Timer {
+ id: resetTimer
+ interval: 1200
+ onTriggered: icon.source = icon.copyUrl
+ }
+ }
+}
diff --git a/src/qml/components/InfoRow.qml b/src/qml/components/InfoRow.qml
index 04d4a9f..f6b83f4 100644
--- a/src/qml/components/InfoRow.qml
+++ b/src/qml/components/InfoRow.qml
@@ -12,6 +12,7 @@ RowLayout {
property string label: ""
property string value: ""
property bool mono: false
+ property bool copyable: false
property color valueColor: Theme.palette.text
spacing: Theme.spacing.medium
@@ -36,4 +37,10 @@ RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
}
+
+ CopyButton {
+ visible: root.copyable && root.value.length > 0
+ value: root.value
+ Layout.alignment: Qt.AlignTop
+ }
}
diff --git a/src/qml/icons/check.svg b/src/qml/icons/check.svg
new file mode 100644
index 0000000..41ec420
--- /dev/null
+++ b/src/qml/icons/check.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/qml/icons/copy.svg b/src/qml/icons/copy.svg
new file mode 100644
index 0000000..3f133fb
--- /dev/null
+++ b/src/qml/icons/copy.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/qml/pages/AccountPage.qml b/src/qml/pages/AccountPage.qml
index a597ce5..32a6ff6 100644
--- a/src/qml/pages/AccountPage.qml
+++ b/src/qml/pages/AccountPage.qml
@@ -49,13 +49,13 @@ Item {
Layout.fillWidth: true
visible: page.loaded && Object.keys(page.account).length > 1
- InfoRow { label: "Account ID"; value: page.account.accountId || ""; mono: true }
+ InfoRow { label: "Account ID"; value: page.account.accountId || ""; mono: true; copyable: true }
InfoRow {
label: "Balance"
value: page.account.balance || "0"
valueColor: Theme.palette.success
}
- InfoRow { label: "Program Owner"; value: page.account.programOwner || ""; mono: true }
+ InfoRow { label: "Program Owner"; value: page.account.programOwner || ""; mono: true; copyable: true }
InfoRow { label: "Nonce"; value: page.account.nonce || "0" }
InfoRow {
label: "Data Size"
diff --git a/src/qml/pages/BlockPage.qml b/src/qml/pages/BlockPage.qml
index cf55d36..9c37d8d 100644
--- a/src/qml/pages/BlockPage.qml
+++ b/src/qml/pages/BlockPage.qml
@@ -49,7 +49,7 @@ Item {
visible: page.loaded && Object.keys(page.block).length > 0
InfoRow { label: "Block ID"; value: String(page.block.blockId !== undefined ? page.block.blockId : "") }
- InfoRow { label: "Hash"; value: page.block.hash || ""; mono: true }
+ InfoRow { label: "Hash"; value: page.block.hash || ""; mono: true; copyable: true }
// Previous hash — a clickable link to the previous block when not genesis.
RowLayout {
@@ -68,6 +68,11 @@ Item {
onTapped: page.explorer.navigateBlock(Number(page.blockId) - 1)
}
}
+ CopyButton {
+ visible: (page.block.prevBlockHash || "").length > 0
+ value: page.block.prevBlockHash || ""
+ Layout.alignment: Qt.AlignTop
+ }
}
InfoRow { label: "Timestamp"; value: page.block.timestampText || "" }
@@ -80,7 +85,7 @@ Item {
Item { Layout.fillWidth: true }
}
- InfoRow { label: "Signature"; value: page.block.signature || ""; mono: true }
+ InfoRow { label: "Signature"; value: page.block.signature || ""; mono: true; copyable: true }
InfoRow { label: "Transactions"; value: String(page.block.txCount !== undefined ? page.block.txCount : 0) }
}
diff --git a/src/qml/pages/TransactionPage.qml b/src/qml/pages/TransactionPage.qml
index b540a6e..35747ba 100644
--- a/src/qml/pages/TransactionPage.qml
+++ b/src/qml/pages/TransactionPage.qml
@@ -48,7 +48,7 @@ Item {
Layout.fillWidth: true
visible: page.loaded && Object.keys(page.tx).length > 0
- InfoRow { label: "Hash"; value: page.tx.hash || ""; mono: true }
+ InfoRow { label: "Hash"; value: page.tx.hash || ""; mono: true; copyable: true }
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.medium
@@ -60,7 +60,7 @@ Item {
// Public.
InfoRow {
visible: page.txType === "Public"
- label: "Program ID"; value: page.tx.programId || ""; mono: true
+ label: "Program ID"; value: page.tx.programId || ""; mono: true; copyable: true
}
InfoRow {
visible: page.txType === "Public"