diff --git a/src/LEZWalletAccountModel.cpp b/src/LEZWalletAccountModel.cpp index 427b404..db0aff4 100644 --- a/src/LEZWalletAccountModel.cpp +++ b/src/LEZWalletAccountModel.cpp @@ -31,6 +31,7 @@ QVariant LEZWalletAccountModel::data(const QModelIndex& index, int role) const case SectionKeyRole: return e.sectionKey; case KeysJsonRole: return e.keysJson; case IsFirstInGroupRole: return e.isFirstInGroup; + case IsFirstPrivateRole: return e.isFirstPrivate; case IsInitializedRole: return e.isInitialized; default: return QVariant(); } @@ -47,6 +48,7 @@ QHash LEZWalletAccountModel::roleNames() const { SectionKeyRole, "sectionKey" }, { KeysJsonRole, "keysJson" }, { IsFirstInGroupRole, "isFirstInGroup" }, + { IsFirstPrivateRole, "isFirstPrivate" }, { IsInitializedRole, "isInitialized" } }; } @@ -100,8 +102,13 @@ void LEZWalletAccountModel::replaceFromVariantList(const QVariantList& list) if (a.isPublic != b.isPublic) return a.isPublic; return a.sectionKey < b.sectionKey; }); - for (int i = 0; i < m_entries.size(); ++i) + for (int i = 0; i < m_entries.size(); ++i) { m_entries[i].isFirstInGroup = (i == 0) || (m_entries[i].sectionKey != m_entries[i - 1].sectionKey); + // All private key-groups sit under a single "Private" title (unlike the public + // section, they don't each get their own top-level header) — so only the very + // first private row across all groups needs to flag it. + m_entries[i].isFirstPrivate = !m_entries[i].isPublic && (i == 0 || m_entries[i - 1].isPublic); + } endResetModel(); if (oldCount != m_entries.size()) emit countChanged(); diff --git a/src/LEZWalletAccountModel.h b/src/LEZWalletAccountModel.h index e3e05e1..37eefec 100644 --- a/src/LEZWalletAccountModel.h +++ b/src/LEZWalletAccountModel.h @@ -18,7 +18,8 @@ struct LEZWalletAccountEntry { bool isPublic = true; QString sectionKey; QString keysJson; // {nullifier_public_key, viewing_public_key} shared by the whole section; private only - bool isFirstInGroup = false; // QML renders the section header above rows where this is true + bool isFirstInGroup = false; // QML renders the per-key-set header (copy button) above rows where this is true + bool isFirstPrivate = false; // QML renders the single "Private" section title above rows where this is true // Whether some program (in practice, the authenticated-transfer program) has claimed // this account yet. Defaults to false (shown as needing init) so an account whose // state we failed to enrich isn't silently mistaken for a usable one. @@ -42,6 +43,7 @@ public: SectionKeyRole, KeysJsonRole, IsFirstInGroupRole, + IsFirstPrivateRole, IsInitializedRole }; Q_ENUM(Role) diff --git a/src/qml/views/AccountsPanel.qml b/src/qml/views/AccountsPanel.qml index 751b661..e7bc949 100644 --- a/src/qml/views/AccountsPanel.qml +++ b/src/qml/views/AccountsPanel.qml @@ -149,26 +149,124 @@ Rectangle { // row is created. property string keysJsonWarm: model.keysJson ?? "" + // "Public Accounts" title: the public section is a single group, so this + // is equivalent to showing it once above the first public row. RowLayout { Layout.fillWidth: true - visible: model.isFirstInGroup ?? false + visible: model.isPublic && (model.isFirstInGroup ?? false) spacing: Theme.spacing.small LogosText { - text: model.isPublic - ? qsTr("Public Accounts") - : qsTr("Private") - font.pixelSize: Theme.typography.secondaryText + text: qsTr("Public Accounts") + font.pixelSize: Theme.typography.primaryText font.bold: true - color: Theme.palette.textSecondary + color: Theme.palette.text + } + } + + // "Private Accounts" title: shown once above the whole private section, + // unlike the per-key-set row below which repeats for every private key + // group. Wrapped the same way as the "Public Accounts" title above so + // both line up identically. + RowLayout { + Layout.fillWidth: true + visible: model.isFirstPrivate ?? false + spacing: Theme.spacing.small + + LogosText { + text: qsTr("Private Accounts") + font.pixelSize: Theme.typography.primaryText + font.bold: true + color: Theme.palette.text + } + } + + // Per-key-set row: separates each private key group within the Private + // section, naming the group by its Npk/Vpk pair, and holds the copy + // button for that pair. + RowLayout { + id: keyGroupHeader + Layout.fillWidth: true + visible: !model.isPublic && (model.isFirstInGroup ?? false) + spacing: Theme.spacing.small + + property var groupKeys: { + try { return JSON.parse(model.keysJson ?? "{}") } catch (e) { return {} } } - Item { Layout.fillWidth: true } + function shortKey(key) { + return key && key.length > 12 ? key.slice(0, 6) + "…" + key.slice(-4) : (key || "") + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + LogosText { + Layout.fillWidth: true + text: qsTr("Accounts under keys") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + + // Each of Npk/Vpk gets its own bullet, aligned with "Private + // Accounts"/"Accounts under keys" above. Labels share a fixed + // width (the wider of the two) so the value column still lines + // up between the two rows. + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + + LogosText { + text: "•" + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + LogosText { + id: npkLabel + Layout.preferredWidth: Math.max(npkLabel.implicitWidth, vpkLabel.implicitWidth) + text: qsTr("Npk:") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + LogosText { + Layout.fillWidth: true + text: keyGroupHeader.shortKey(keyGroupHeader.groupKeys.nullifier_public_key) + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + elide: Text.ElideRight + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + + LogosText { + text: "•" + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + LogosText { + id: vpkLabel + Layout.preferredWidth: Math.max(npkLabel.implicitWidth, vpkLabel.implicitWidth) + text: qsTr("Vpk:") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + LogosText { + Layout.fillWidth: true + text: keyGroupHeader.shortKey(keyGroupHeader.groupKeys.viewing_public_key) + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + elide: Text.ElideRight + } + } + } LogosCopyButton { Layout.preferredHeight: 32 Layout.preferredWidth: 32 - visible: !model.isPublic icon.color: Theme.palette.textMuted onCopyText: root.copyRequested(model.keysJson ?? "") }