diff --git a/mobile-app/lib/l10n/app_en.arb b/mobile-app/lib/l10n/app_en.arb index 21fad536..4a5e03b4 100644 --- a/mobile-app/lib/l10n/app_en.arb +++ b/mobile-app/lib/l10n/app_en.arb @@ -1105,6 +1105,10 @@ "@accountMenuAddressDetails": { "description": "Address details menu row label" }, + "accountMenuInnerHash": "Inner Hash", + "@accountMenuInnerHash": { + "description": "Menu row label shown for encrypted accounts instead of address details" + }, "accountMenuShowRecoveryPhrase": "Show Recovery Phrase", "@accountMenuShowRecoveryPhrase": { "description": "Show recovery phrase menu row label" @@ -1212,6 +1216,23 @@ "description": "App bar title on account details screen" }, + "innerHashTitle": "Inner Hash", + "@innerHashTitle": { + "description": "App bar title on the encrypted account inner hash screen" + }, + "innerHashLabel": "INNER HASH", + "@innerHashLabel": { + "description": "Card label above the inner hash value" + }, + "innerHashCopied": "Inner hash copied", + "@innerHashCopied": { + "description": "Toaster shown after copying the inner hash" + }, + "innerHashLoadError": "Couldn't load inner hash", + "@innerHashLoadError": { + "description": "Toaster shown when deriving the inner hash fails" + }, + "addKeystoneAppBarTitle": "Add Keystone Wallet", "@addKeystoneAppBarTitle": { "description": "App bar title for the add Keystone wallet screens" diff --git a/mobile-app/lib/l10n/app_id.arb b/mobile-app/lib/l10n/app_id.arb index fefbff5d..90e954be 100644 --- a/mobile-app/lib/l10n/app_id.arb +++ b/mobile-app/lib/l10n/app_id.arb @@ -239,6 +239,7 @@ "accountMenuTitle": "Akun", "accountMenuAccountName": "Nama Akun", "accountMenuAddressDetails": "Detail Alamat", + "accountMenuInnerHash": "Inner Hash", "accountMenuShowRecoveryPhrase": "Tampilkan Recovery Phrase", "accountMenuNotFound": "Akun tidak ditemukan", "accountMenuDone": "Selesai", @@ -257,6 +258,10 @@ "accountMenuDeleteWalletMessage": "Frasa pemulihan Dompet {number} akan dihapus permanen dari perangkat ini. Pastikan sudah dicadangkan — ini tidak dapat dibatalkan.", "accountMenuDeleteWalletConfirm": "Hapus Dompet", "accountDetailsTitle": "Detail Alamat", + "innerHashTitle": "Inner Hash", + "innerHashLabel": "INNER HASH", + "innerHashCopied": "Inner hash disalin", + "innerHashLoadError": "Gagal memuat inner hash", "invalidAddress": "Alamat tidak valid", "sendTitle": "Kirim", "sendPayTitle": "Bayar", diff --git a/mobile-app/lib/l10n/app_localizations.dart b/mobile-app/lib/l10n/app_localizations.dart index 52410baa..15a0b84b 100644 --- a/mobile-app/lib/l10n/app_localizations.dart +++ b/mobile-app/lib/l10n/app_localizations.dart @@ -1532,6 +1532,12 @@ abstract class AppLocalizations { /// **'Address Details'** String get accountMenuAddressDetails; + /// Menu row label shown for encrypted accounts instead of address details + /// + /// In en, this message translates to: + /// **'Inner Hash'** + String get accountMenuInnerHash; + /// Show recovery phrase menu row label /// /// In en, this message translates to: @@ -1640,6 +1646,30 @@ abstract class AppLocalizations { /// **'Address Details'** String get accountDetailsTitle; + /// App bar title on the encrypted account inner hash screen + /// + /// In en, this message translates to: + /// **'Inner Hash'** + String get innerHashTitle; + + /// Card label above the inner hash value + /// + /// In en, this message translates to: + /// **'INNER HASH'** + String get innerHashLabel; + + /// Toaster shown after copying the inner hash + /// + /// In en, this message translates to: + /// **'Inner hash copied'** + String get innerHashCopied; + + /// Toaster shown when deriving the inner hash fails + /// + /// In en, this message translates to: + /// **'Couldn\'t load inner hash'** + String get innerHashLoadError; + /// App bar title for the add Keystone wallet screens /// /// In en, this message translates to: diff --git a/mobile-app/lib/l10n/app_localizations_en.dart b/mobile-app/lib/l10n/app_localizations_en.dart index d9244f1d..55622304 100644 --- a/mobile-app/lib/l10n/app_localizations_en.dart +++ b/mobile-app/lib/l10n/app_localizations_en.dart @@ -792,6 +792,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get accountMenuAddressDetails => 'Address Details'; + @override + String get accountMenuInnerHash => 'Inner Hash'; + @override String get accountMenuShowRecoveryPhrase => 'Show Recovery Phrase'; @@ -858,6 +861,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get accountDetailsTitle => 'Address Details'; + @override + String get innerHashTitle => 'Inner Hash'; + + @override + String get innerHashLabel => 'INNER HASH'; + + @override + String get innerHashCopied => 'Inner hash copied'; + + @override + String get innerHashLoadError => 'Couldn\'t load inner hash'; + @override String get addKeystoneAppBarTitle => 'Add Keystone Wallet'; diff --git a/mobile-app/lib/l10n/app_localizations_id.dart b/mobile-app/lib/l10n/app_localizations_id.dart index 226a6478..0b45ae19 100644 --- a/mobile-app/lib/l10n/app_localizations_id.dart +++ b/mobile-app/lib/l10n/app_localizations_id.dart @@ -795,6 +795,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get accountMenuAddressDetails => 'Detail Alamat'; + @override + String get accountMenuInnerHash => 'Inner Hash'; + @override String get accountMenuShowRecoveryPhrase => 'Tampilkan Recovery Phrase'; @@ -861,6 +864,18 @@ class AppLocalizationsId extends AppLocalizations { @override String get accountDetailsTitle => 'Detail Alamat'; + @override + String get innerHashTitle => 'Inner Hash'; + + @override + String get innerHashLabel => 'INNER HASH'; + + @override + String get innerHashCopied => 'Inner hash disalin'; + + @override + String get innerHashLoadError => 'Gagal memuat inner hash'; + @override String get addKeystoneAppBarTitle => 'Add Keystone Wallet'; diff --git a/mobile-app/lib/v2/components/address_details_card.dart b/mobile-app/lib/v2/components/address_details_card.dart index 490c9fb1..3a2cf482 100644 --- a/mobile-app/lib/v2/components/address_details_card.dart +++ b/mobile-app/lib/v2/components/address_details_card.dart @@ -1,146 +1,32 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:resonance_network_wallet/l10n/app_localizations.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/l10n_provider.dart'; -import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; +import 'package:resonance_network_wallet/v2/components/copyable_data_item.dart'; import 'package:resonance_network_wallet/v2/components/split_card.dart'; -import 'package:quantus_sdk/quantus_sdk.dart'; -class AddressDetailsCard extends ConsumerStatefulWidget { +class AddressDetailsCard extends ConsumerWidget { final String accountId; final String? checksum; const AddressDetailsCard({super.key, required this.accountId, this.checksum}); @override - ConsumerState createState() => _AddressDetailsCardState(); -} - -class _AddressDetailsCardState extends ConsumerState { - bool _addressCopied = false; - bool _checksumCopied = false; - Timer? _resetTimer; - - void _copyAddress(BuildContext context) { - context.copyTextWithToaster(widget.accountId); - _triggerCopied(isAddress: true); - } - - void _copyChecksum(BuildContext context, AppLocalizations l10n) { - if (widget.checksum == null) return; - - context.copyTextWithToaster(widget.checksum!, message: l10n.componentCheckphraseCopied); - _triggerCopied(isAddress: false); - } - - void _triggerCopied({required bool isAddress}) { - _resetTimer?.cancel(); - - setState(() { - if (isAddress) { - _addressCopied = true; - _checksumCopied = false; - } else { - _checksumCopied = true; - _addressCopied = false; - } - }); - - _resetTimer = Timer(const Duration(seconds: 2), () { - if (mounted) { - setState(() { - if (isAddress) { - _addressCopied = false; - } else { - _checksumCopied = false; - } - }); - } - }); - } - - @override - void dispose() { - _resetTimer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final l10n = ref.watch(l10nProvider); return SplitCard( - topChild: InkWell( - onTap: () => _copyAddress(context), - child: _buildItem(context, l10n.componentAddressLabel, widget.accountId, isCopied: _addressCopied), - ), - bottomChild: InkWell( - onTap: () => _copyChecksum(context, l10n), - child: _buildItem( - context, - l10n.componentCheckphraseLabel, - widget.checksum ?? l10n.commonLoading, - isCheckphrase: true, - isCopied: _checksumCopied, - ), - ), - ); - } - - Widget _buildItem( - BuildContext context, - String label, - String value, { - bool isCheckphrase = false, - required bool isCopied, - }) { - final colors = context.colorsV3; - final text = context.themeTextV3; - final valueTextStyle = isCheckphrase - ? text.dataAddressLarge.copyWith(color: colors.semanticLilac) - : text.dataAddressLarge.copyWith(color: colors.textContent); - - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: text.labelData.copyWith(color: colors.textMuted)), - const SizedBox(height: 16), - Text(value, style: valueTextStyle), - ], - ), - ), - const SizedBox(width: 32), - _copyButton(isCopied: isCopied), - ], - ); - } - - Widget _copyButton({required bool isCopied}) { - const containerSize = 40.0; - const iconSize = 16.0; - final colors = context.colorsV3; - - return AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: containerSize, - height: containerSize, - decoration: BoxDecoration( - color: isCopied ? colors.semanticSage.useOpacity(0.08) : Colors.transparent, - shape: BoxShape.circle, - border: Border.all(color: isCopied ? colors.semanticSage.useOpacity(0.15) : colors.borderHairline, width: 1), + topChild: CopyableDataItem( + label: l10n.componentAddressLabel, + value: accountId, + copiedMessage: l10n.receiveCopiedMessage, ), - child: Center( - child: Icon( - isCopied ? Icons.check : Icons.copy, - size: iconSize, - color: isCopied ? colors.semanticSage : colors.textContent, - ), + bottomChild: CopyableDataItem( + label: l10n.componentCheckphraseLabel, + value: checksum ?? l10n.commonLoading, + copiedMessage: l10n.componentCheckphraseCopied, + valueColor: context.colorsV3.semanticLilac, + enabled: checksum != null, ), ); } diff --git a/mobile-app/lib/v2/components/copyable_data_item.dart b/mobile-app/lib/v2/components/copyable_data_item.dart new file mode 100644 index 00000000..039cf3c6 --- /dev/null +++ b/mobile-app/lib/v2/components/copyable_data_item.dart @@ -0,0 +1,109 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; + +/// Label + value row with a copy button that shows a check mark for two +/// seconds after the value is copied. +class CopyableDataItem extends StatefulWidget { + final String label; + final String value; + final String copiedMessage; + final Color? valueColor; + final bool enabled; + + const CopyableDataItem({ + super.key, + required this.label, + required this.value, + required this.copiedMessage, + this.valueColor, + this.enabled = true, + }); + + @override + State createState() => _CopyableDataItemState(); +} + +class _CopyableDataItemState extends State { + bool _copied = false; + Timer? _resetTimer; + + void _copy() { + if (!widget.enabled) return; + context.copyTextWithToaster(widget.value, message: widget.copiedMessage); + _resetTimer?.cancel(); + setState(() => _copied = true); + _resetTimer = Timer(const Duration(seconds: 2), () { + if (mounted) setState(() => _copied = false); + }); + } + + @override + void dispose() { + _resetTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colors = context.colorsV3; + final text = context.themeTextV3; + + return InkWell( + onTap: _copy, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.label, style: text.labelData.copyWith(color: colors.textMuted)), + const SizedBox(height: 16), + Text( + widget.value, + style: text.dataAddressLarge.copyWith(color: widget.valueColor ?? colors.textContent), + ), + ], + ), + ), + const SizedBox(width: 32), + _CopyButton(isCopied: _copied), + ], + ), + ); + } +} + +class _CopyButton extends StatelessWidget { + final bool isCopied; + + const _CopyButton({required this.isCopied}); + + @override + Widget build(BuildContext context) { + const containerSize = 40.0; + const iconSize = 16.0; + final colors = context.colorsV3; + + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: containerSize, + height: containerSize, + decoration: BoxDecoration( + color: isCopied ? colors.semanticSage.useOpacity(0.08) : Colors.transparent, + shape: BoxShape.circle, + border: Border.all(color: isCopied ? colors.semanticSage.useOpacity(0.15) : colors.borderHairline, width: 1), + ), + child: Center( + child: Icon( + isCopied ? Icons.check : Icons.copy, + size: iconSize, + color: isCopied ? colors.semanticSage : colors.textContent, + ), + ), + ); + } +} diff --git a/mobile-app/lib/v2/components/menu_row.dart b/mobile-app/lib/v2/components/menu_row.dart index c29ecb01..63668a55 100644 --- a/mobile-app/lib/v2/components/menu_row.dart +++ b/mobile-app/lib/v2/components/menu_row.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +/// Menu row; without [onTap] it is static and shows no chevron. class MenuRow extends StatelessWidget { final String label; final String? value; - final VoidCallback onTap; + final VoidCallback? onTap; - const MenuRow({super.key, required this.label, this.value, required this.onTap}); + const MenuRow({super.key, required this.label, this.value, this.onTap}); @override Widget build(BuildContext context) { @@ -31,7 +32,7 @@ class MenuRow extends StatelessWidget { Text(value!, style: text.body.copyWith(color: colors.textMuted)), const SizedBox(width: 4), ], - QuantusIcon(QuantusIcons.chevronRight, color: colors.textMuted), + if (onTap != null) QuantusIcon(QuantusIcons.chevronRight, color: colors.textMuted), ], ), ], diff --git a/mobile-app/lib/v2/components/split_card.dart b/mobile-app/lib/v2/components/split_card.dart index 3ca6c2c2..08e664bf 100644 --- a/mobile-app/lib/v2/components/split_card.dart +++ b/mobile-app/lib/v2/components/split_card.dart @@ -3,14 +3,18 @@ import 'package:quantus_sdk/quantus_sdk.dart'; class SplitCard extends StatelessWidget { final Widget topChild; - final Widget bottomChild; + final Widget? bottomChild; - const SplitCard({super.key, required this.topChild, required this.bottomChild}); + const SplitCard({super.key, required this.topChild, required Widget this.bottomChild}); + + const SplitCard.single({super.key, required Widget child}) : topChild = child, bottomChild = null; @override Widget build(BuildContext context) { final colors = context.colorsV3; - final topPadding = const EdgeInsets.only(top: 32, bottom: 24, left: 24, right: 24); + final topPadding = bottomChild == null + ? const EdgeInsets.symmetric(vertical: 32, horizontal: 24) + : const EdgeInsets.only(top: 32, bottom: 24, left: 24, right: 24); final bottomPadding = const EdgeInsets.only(top: 24, bottom: 32, left: 24, right: 24); return Column( @@ -21,8 +25,10 @@ class SplitCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container(padding: topPadding, child: topChild), - Divider(color: colors.bgVoid, thickness: 4), - Container(padding: bottomPadding, child: bottomChild), + if (bottomChild != null) ...[ + Divider(color: colors.bgVoid, thickness: 4), + Container(padding: bottomPadding, child: bottomChild), + ], ], ), ), diff --git a/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart b/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart index dbe184a7..0951022f 100644 --- a/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/account_menu_screen.dart @@ -12,7 +12,9 @@ import 'package:resonance_network_wallet/v2/components/account_badge.dart'; import 'package:resonance_network_wallet/v2/components/menu_row.dart'; import 'package:resonance_network_wallet/v2/screens/accounts/account_details_screen.dart'; import 'package:resonance_network_wallet/v2/screens/accounts/accounts_navigation.dart'; +import 'package:resonance_network_wallet/v2/components/private_activity_notice.dart'; import 'package:resonance_network_wallet/v2/screens/accounts/edit_account_screen.dart'; +import 'package:resonance_network_wallet/v2/screens/accounts/inner_hash_screen.dart'; import 'package:resonance_network_wallet/v2/screens/settings/recovery_phrase_confirmation_screen.dart'; class AccountMenuScreen extends ConsumerWidget { @@ -30,6 +32,7 @@ class AccountMenuScreen extends ConsumerWidget { final accounts = ref.watch(accountsProvider); final account = accounts.value?.firstWhereOrNull((a) => a.accountId == initialAccount.accountId) ?? initialAccount; + final isEncrypted = account.accountType == AccountType.encrypted; final canShowRecoveryPhrase = account.accountType == AccountType.local; return ScaffoldBase( @@ -43,12 +46,15 @@ class AccountMenuScreen extends ConsumerWidget { MenuRow( label: l10n.accountMenuAccountName, value: account.name, - onTap: () => _openNameEditor(context, ref, account), + onTap: isEncrypted ? null : () => _openNameEditor(context, ref, account), ), const SizedBox(height: 4), const MenuDivider(), const SizedBox(height: 12), - MenuRow(label: l10n.accountMenuAddressDetails, onTap: () => _openAddressDetails(context, account)), + if (isEncrypted) + MenuRow(label: l10n.accountMenuInnerHash, onTap: () => _openInnerHash(context, account)) + else + MenuRow(label: l10n.accountMenuAddressDetails, onTap: () => _openAddressDetails(context, account)), if (canShowRecoveryPhrase) ...[ const SizedBox(height: 4), const MenuDivider(), @@ -205,6 +211,12 @@ class AccountMenuScreen extends ConsumerWidget { context, ).push(MaterialPageRoute(builder: (_) => AccountDetailsScreen(accountId: account.accountId))); } + + void _openInnerHash(BuildContext context, Account account) { + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => InnerHashScreen(walletIndex: account.walletIndex))); + } } class _ProfileHeader extends StatelessWidget { @@ -219,7 +231,10 @@ class _ProfileHeader extends StatelessWidget { return Column( children: [ - AccountBadge.account(account: account, isActive: true, size: 96, textStyle: text.titleHero), + if (account.accountType == AccountType.encrypted) + const EncryptedLockBadge(size: 96) + else + AccountBadge.account(account: account, isActive: true, size: 96, textStyle: text.titleHero), const SizedBox(height: 12), Text( account.name, diff --git a/mobile-app/lib/v2/screens/accounts/accounts_screen.dart b/mobile-app/lib/v2/screens/accounts/accounts_screen.dart index a2e135f7..92c52c25 100644 --- a/mobile-app/lib/v2/screens/accounts/accounts_screen.dart +++ b/mobile-app/lib/v2/screens/accounts/accounts_screen.dart @@ -441,6 +441,7 @@ class _AccountsScreenState extends ConsumerState { leading: const EncryptedLockBadge(), title: account.name, subtitle: _balanceText(l10n, account), + trailing: _MenuKebab(onTap: () => _openAccountMenu(account)), ); } diff --git a/mobile-app/lib/v2/screens/accounts/inner_hash_screen.dart b/mobile-app/lib/v2/screens/accounts/inner_hash_screen.dart new file mode 100644 index 00000000..7bd672e4 --- /dev/null +++ b/mobile-app/lib/v2/screens/accounts/inner_hash_screen.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quantus_sdk/quantus_sdk.dart' hide ScaffoldBase; +import 'package:resonance_network_wallet/providers/l10n_provider.dart'; +import 'package:resonance_network_wallet/providers/wallet_providers.dart'; +import 'package:resonance_network_wallet/shared/utils/print.dart'; +import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; +import 'package:resonance_network_wallet/v2/components/copyable_data_item.dart'; +import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; +import 'package:resonance_network_wallet/v2/components/share_account_button.dart'; +import 'package:resonance_network_wallet/v2/components/split_card.dart'; + +/// Inner hash of the encrypted account's current receive address: the value a +/// miner passes as `--rewards-inner-hash` so block rewards land on this account. +class InnerHashScreen extends ConsumerStatefulWidget { + const InnerHashScreen({super.key, required this.walletIndex}); + + final int walletIndex; + + @override + ConsumerState createState() => _InnerHashScreenState(); +} + +class _InnerHashScreenState extends ConsumerState { + String? _innerHash; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + try { + final keyPair = await ref.read(encryptedAccountServiceProvider(widget.walletIndex)).receiveKeyPair(); + if (mounted) setState(() => _innerHash = keyPair.rewardsPreimageHex); + } catch (e, st) { + quantusPrint('[InnerHashScreen] load failed: $e\n$st'); + if (mounted) context.showErrorToaster(message: ref.read(l10nProvider).innerHashLoadError); + } + } + + void _share() { + final innerHash = _innerHash; + if (innerHash == null) return; + shareText(context, innerHash); + } + + @override + Widget build(BuildContext context) { + final l10n = ref.watch(l10nProvider); + + return ScaffoldBase( + appBar: V2AppBar(title: l10n.innerHashTitle), + mainContent: SplitCard.single( + child: CopyableDataItem( + label: l10n.innerHashLabel, + value: _innerHash ?? l10n.commonLoading, + copiedMessage: l10n.innerHashCopied, + enabled: _innerHash != null, + ), + ), + bottomContent: ScaffoldBaseBottomContent( + child: ShareAccountButton(onTap: _share, isDisabled: _innerHash == null), + ), + ); + } +} diff --git a/quantus_sdk/lib/src/ui/components/back_button.dart b/quantus_sdk/lib/src/ui/components/back_button.dart index 56eb4602..2a3ff1a2 100644 --- a/quantus_sdk/lib/src/ui/components/back_button.dart +++ b/quantus_sdk/lib/src/ui/components/back_button.dart @@ -1,24 +1,36 @@ import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +/// 28pt glass chevron with a [hitSize] square tap target; the visual sits at +/// the left edge so the extra area extends right, above and below it. class AppBackButton extends StatelessWidget { + static const double hitSize = 48; + final VoidCallback? onTap; const AppBackButton({super.key, this.onTap}); @override Widget build(BuildContext context) { - final double containerSize = 28; - final double iconSize = 24; + const double containerSize = 28; + const double iconSize = 24; return GestureDetector( onTap: onTap ?? () => Navigator.pop(context), - child: GlassButtonBase( - buttonHeight: containerSize, - buttonWidth: containerSize, - borderRadius: BorderRadius.circular(containerSize / 2), - padding: const EdgeInsets.all(2), - child: QuantusIcon(QuantusIcons.chevronLeft, size: iconSize, color: context.colorsV3.textContent), + behavior: HitTestBehavior.opaque, + child: SizedBox( + width: hitSize, + height: hitSize, + child: Align( + alignment: Alignment.centerLeft, + child: GlassButtonBase( + buttonHeight: containerSize, + buttonWidth: containerSize, + borderRadius: BorderRadius.circular(containerSize / 2), + padding: const EdgeInsets.all(2), + child: QuantusIcon(QuantusIcons.chevronLeft, size: iconSize, color: context.colorsV3.textContent), + ), + ), ), ); } diff --git a/quantus_sdk/lib/src/ui/components/v2_app_bar.dart b/quantus_sdk/lib/src/ui/components/v2_app_bar.dart index f81b3302..e76f60a1 100644 --- a/quantus_sdk/lib/src/ui/components/v2_app_bar.dart +++ b/quantus_sdk/lib/src/ui/components/v2_app_bar.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; +/// Title bar with equal [AppBackButton.hitSize] side slots so the title stays +/// centred whatever sits in them. Default padding keeps the bar 76pt tall. class V2AppBar extends StatelessWidget { final String title; final Widget? leading; @@ -14,23 +16,27 @@ class V2AppBar extends StatelessWidget { this.leading, this.trailing, this.showBackButton = true, - this.padding = const EdgeInsets.only(top: 16.0, bottom: 32.0), + this.padding = const EdgeInsets.only(top: 6.0, bottom: 22.0), }); + Widget _slot(Widget? child, Alignment alignment) => SizedBox( + width: AppBackButton.hitSize, + height: AppBackButton.hitSize, + child: child == null ? null : Align(alignment: alignment, child: child), + ); + @override Widget build(BuildContext context) { final colors = context.colorsV3; final text = context.themeTextV3; - Widget leftWidget = leading ?? (showBackButton ? const AppBackButton() : const SizedBox(width: 24)); - Widget rightWidget = trailing ?? const SizedBox(width: 24); + final leftWidget = leading ?? (showBackButton ? const AppBackButton() : null); return Padding( padding: padding, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - leftWidget, + _slot(leftWidget, Alignment.centerLeft), Expanded( child: FittedBox( fit: BoxFit.scaleDown, @@ -41,7 +47,7 @@ class V2AppBar extends StatelessWidget { ), ), ), - rightWidget, + _slot(trailing, Alignment.centerRight), ], ), );