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"