a11y: dashboard and navigation semantics (navbar, cards, sync, switchers) (#3468)

* Label new-UI dashboard navigation and sync status for screen readers

Each bottom-nav tab becomes one button node carrying its localized name, its
selected state and mutual exclusion; the pill text no longer duplicates the
selected tab. The assets/history tabs gain selected state, the Bitcoin/Lightning
switcher becomes a labeled toggle, and the sync bar becomes a single button whose
label is the localized status (plus Tor/MWEB/Silent Payments badges) with a hint
pointing at node management. Compact mode's pulsing dot and the chain icon's
progress ring now have text equivalents, and the invisible pointer-absorbing
strip behind the nav bar is excluded from traversal.

* Make new-UI dashboard cards, lists and menus operable by screen readers

Collapse the wallet-name row into one labeled button (hardware-wallet glyph
folded into the label, inner accounts button no longer a second stop) and give
balance cards a labeled, selectable node with a long-press action for show/hide
balance. Balances hidden behind AnimatedOpacity, card artwork and decorative
glyphs leave the semantics tree; the 3-dots customize target and the card action
buttons are now named buttons. Assets/history rows, the history header, the MWEB
promo, long-press menu items, the account cards and the add-account and
generate-name controls become single button nodes, the phantom action chip in the
assets header is built conditionally instead of drawn at opacity 0, and app cards
get a close tooltip plus an 'opens externally' hint.

* Use expression bodies for the new a11y helpers

* Restore untouched expressions in cards_view

* Address review: state-aware balance hint, tooltip-free close label

* Address review: keep header height without the chip, name the selected index
This commit is contained in:
Seth For Privacy
2026-08-03 19:58:51 +02:00
committed by GitHub
parent 8d38381693
commit a6bcf7c97a
19 changed files with 1010 additions and 750 deletions
+12 -8
View File
@@ -90,14 +90,18 @@ class _NewDashboardState extends State<NewDashboard> {
),
),
),
SafeArea(
bottom: !(Platform.isIOS),
child: SizedBox(
width: double.infinity,
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
child: AbsorbPointer(
absorbing: true,
child: Container(color: Colors.transparent),
// Invisible pointer-absorbing strip behind the nav bar: it must not
// be reachable by screen-reader traversal either.
ExcludeSemantics(
child: SafeArea(
bottom: !(Platform.isIOS),
child: SizedBox(
width: double.infinity,
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
child: AbsorbPointer(
absorbing: true,
child: Container(color: Colors.transparent),
),
),
),
),
+68 -44
View File
@@ -174,17 +174,29 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
itemCount: _items.length,
itemBuilder: (BuildContext context, int index) {
final card = _items[index].card;
// The stack is ordered bottom to top, so the last item
// is the account currently in front — the selected one.
final selectedItemIndex = _items.length - 1;
return Container(
key: ValueKey(index),
child: GestureDetector(
onTap: () {
reorder(index, _items.length);
},
child: Align(
alignment: Alignment.topCenter,
heightFactor: _kStackVisibleFactor,
child: card,
// One labeled, selectable node per account; the card's own
// texts stay reachable underneath it.
child: Semantics(
button: true,
selected: selectedItemIndex == index,
label: _items[index].accountListItem.label,
onTap: () => reorder(index, _items.length),
child: GestureDetector(
excludeFromSemantics: true,
onTap: () {
reorder(index, _items.length);
},
child: Align(
alignment: Alignment.topCenter,
heightFactor: _kStackVisibleFactor,
child: card,
),
),
),
);
@@ -199,31 +211,36 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(999999),
onTap: _showAddAccountModal,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.add,
size: 28,
color: Theme.of(context).colorScheme.primary,
),
Text(
S.of(context).add_account,
style: TextStyle(
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
borderRadius: BorderRadius.circular(999999),
onTap: _showAddAccountModal,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.add,
size: 28,
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500),
)
],
),
Text(
S.of(context).add_account,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500),
)
],
),
),
),
),
),
@@ -406,6 +423,8 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
final TextEditingController _controller = TextEditingController();
bool _loading = false;
Future<void> _generateAccountName() async => _controller.text = await generateName();
@override
Widget build(BuildContext context) {
return Container(
@@ -441,18 +460,23 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
),
Padding(
padding: const EdgeInsets.all(12.0),
child: GestureDetector(
onTap: () async {
_controller.text = await generateName();
},
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(5)),
child: CakeImageWidget(
imageUrl: "assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
child: Semantics(
button: true,
label: S.of(context).generate_name,
onTap: _generateAccountName,
child: ExcludeSemantics(
child: GestureDetector(
onTap: _generateAccountName,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(5)),
child: CakeImageWidget(
imageUrl: "assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
),
),
),
),
+79 -57
View File
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/core/theme_extension.dart';
import 'package:flutter/material.dart';
@@ -60,71 +61,86 @@ class AppsWidget extends StatelessWidget {
end: Alignment.bottomCenter,
),
),
child: TextButton(
onPressed: onTap,
style: TextButton.styleFrom(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(
width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
),
padding: EdgeInsets.all(24),
),
child: Column(
children: [
Row(
// The card is one node; the arrow glyph's "leaves the app" meaning
// becomes a hint instead of an unlabeled icon.
child: MergeSemantics(
child: Semantics(
hint: isLink == true ? S.of(context).opens_externally : null,
child: TextButton(
onPressed: onTap,
style: TextButton.styleFrom(
shape: RoundedSuperellipseBorder(
borderRadius: BorderRadius.circular(18),
side: BorderSide(
width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
),
padding: EdgeInsets.all(24),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(right: 20),
child: CakeImageWidget(imageUrl: image, height: 54, width: 54),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 6.0,
Row(
children: [
Padding(
padding: const EdgeInsets.only(right: 20),
child: ExcludeSemantics(
child: CakeImageWidget(imageUrl: image, height: 54, width: 54),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 6.0,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w800,
fontSize: 20,
),
softWrap: true,
),
isCake == true
? ExcludeSemantics(
child: CakeImageWidget(
imageUrl: "assets/new-ui/cakelabs-icon.svg",
color: Theme.of(context)
.colorScheme
.onSurfaceVariant),
)
: SizedBox(),
],
),
SizedBox(height: 5),
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w800,
fontSize: 20,
subTitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
softWrap: true,
),
isCake == true
? CakeImageWidget(
imageUrl: "assets/new-ui/cakelabs-icon.svg",
color: Theme.of(context).colorScheme.onSurfaceVariant)
: SizedBox(),
],
),
SizedBox(height: 5),
Text(
subTitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
softWrap: true,
),
ExcludeSemantics(
child: Icon(
isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
),
],
),
)
],
),
Icon(
isLink == true ? Icons.arrow_outward : Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
)
if (hint != null) ...[
SizedBox(height: 10),
hint!,
]
],
),
if (hint != null) ...[
SizedBox(height: 10),
hint!,
]
],
),
),
),
),
@@ -132,10 +148,16 @@ class AppsWidget extends StatelessWidget {
Positioned(
top: 10,
right: 10,
child: IconButton(
icon: Icon(Icons.close),
onPressed: onClose,
//color: Theme.of(context).colorScheme.onSurface,
// Label the icon-only button without adding a visible tooltip.
child: MergeSemantics(
child: Semantics(
label: S.of(context).close,
child: IconButton(
icon: Icon(Icons.close),
onPressed: onClose,
//color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
@@ -39,125 +39,138 @@ class AssetTile extends StatelessWidget {
Widget build(BuildContext context) {
final iconPath = balance.asset.iconPath ?? "";
return GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return AssetDetailsModal(
showSwap: showSwap,
showBridgeButton: showBridgeButton,
asset: balance.asset,
title: title ?? balance.asset.fullName ?? balance.asset.name,
chainTitle: "",
subtitle: trailingText ?? _getChainTitle(),
amount: showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
currencyTitle: balance.asset.title,
fiatAmount: showSecondary
? balance.fiatSecondAvailableBalance
: balance.fiatAvailableBalance,
iconPath: balance.asset.iconPath ?? "",
chainIconPath: chainIconPath,
mode: modalMode,
wallet: wallet,
);
});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Container(
width: double.infinity,
height: 72,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: isFirst ? Radius.circular(18) : Radius.zero,
bottom: isLast ? Radius.circular(18) : Radius.zero,
),
),
// The row is one control: name, amount and fiat value merge into a single
// button node that opens the asset details sheet.
return MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return AssetDetailsModal(
showSwap: showSwap,
showBridgeButton: showBridgeButton,
asset: balance.asset,
title: title ?? balance.asset.fullName ?? balance.asset.name,
chainTitle: "",
subtitle: trailingText ?? _getChainTitle(),
amount:
showSecondary ? balance.secondAvailableBalance : balance.availableBalance,
currencyTitle: balance.asset.title,
fiatAmount: showSecondary
? balance.fiatSecondAvailableBalance
: balance.fiatAvailableBalance,
iconPath: balance.asset.iconPath ?? "",
chainIconPath: chainIconPath,
mode: modalMode,
wallet: wallet,
);
});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
iconPath.isNotEmpty
? TokenImageWidget(
imageUrl: iconPath,
size: 36,
)
: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
balance.asset.name
.substring(0, min(2, balance.asset.name.length)),
style: TextStyle(
fontSize: 20,
color: Theme.of(context).colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Container(
width: double.infinity,
height: 72,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: isFirst ? Radius.circular(18) : Radius.zero,
bottom: isLast ? Radius.circular(18) : Radius.zero,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Decorative: the asset name is already in the row text.
ExcludeSemantics(
child: iconPath.isNotEmpty
? TokenImageWidget(
imageUrl: iconPath,
size: 36,
)
: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
balance.asset.name
.substring(0, min(2, balance.asset.name.length)),
style: TextStyle(
fontSize: 20,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
),
),
),
),
SizedBox(width: 12.0),
Expanded(
child: Column(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 4,
),
SizedBox(width: 12.0),
Expanded(
child: Column(
spacing: 4.0,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title ?? balance.asset.fullName ?? balance.asset.name,
style: TextStyle(fontWeight: FontWeight.w500),
Row(
spacing: 4,
children: [
Text(
title ?? balance.asset.fullName ?? balance.asset.name,
style: TextStyle(fontWeight: FontWeight.w500),
),
if (trailingText != null)
Text(
trailingText!,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
if (trailingText != null)
Text(
trailingText!,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant),
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
"${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
maxLines: 1,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
],
),
Padding(
padding: const EdgeInsets.only(right: 4.0),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
"${showSecondary ? balance.secondAvailableBalance : balance.availableBalance} ${balance.formattedAssetTitle.safeSubString(0, 6)}",
maxLines: 1,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
],
),
),
],
),
],
),
),
Text(
showSecondary
? balance.fiatSecondAvailableBalance
: balance.fiatAvailableBalance,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
Text(
showSecondary ? balance.fiatSecondAvailableBalance : balance.fiatAvailableBalance,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
),
),
@@ -22,6 +22,8 @@ class AssetsTopBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final actionButton = tabs[selectedTab].actionButton;
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 32.0, bottom: 0.0, left: 12.0, right: 18.0),
@@ -49,40 +51,49 @@ class AssetsTopBar extends StatelessWidget {
key: ValueKey(selectedTab),
spacing: 8,
children: [
Opacity(
opacity: tabs[selectedTab].actionButton != null ? 1 : 0,
child: GestureDetector(
onTap: () {
if (tabs[selectedTab].actionButton != null) {
tabs[selectedTab].actionButton?.onPressed();
}
},
child: Container(
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999999),
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
spacing: 6,
children: [
if ((tabs[selectedTab].actionButton?.title ?? "").isNotEmpty)
Text(
tabs[selectedTab].actionButton?.title ?? "",
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
CakeImageWidget(
imageUrl: tabs[selectedTab].actionButton?.iconPath,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn)),
],
// Built conditionally rather than rendered at opacity 0: an
// invisible chip stayed focusable for screen readers. The
// SizedBox keeps the 40px header height the invisible chip
// used to occupy.
if (actionButton == null)
const SizedBox(height: 40)
else
MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(
onTap: actionButton.onPressed,
child: Container(
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999999),
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
spacing: 6,
children: [
if (actionButton.title.isNotEmpty)
Text(
actionButton.title,
style:
TextStyle(color: Theme.of(context).colorScheme.primary),
),
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: actionButton.iconPath,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn)),
),
],
),
),
),
),
),
),
),
],
),
),
@@ -36,6 +36,15 @@ class HistorySection extends StatelessWidget {
final bool roundedTopSection;
final bool detailsAsPage;
/// A history row is a single button node: every text inside it (direction,
/// date, amounts) merges into one label.
Widget _historyRow({required VoidCallback onTap, required Widget child}) => MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(onTap: onTap, child: child),
),
);
@override
Widget build(BuildContext context) {
return SliverPadding(
@@ -90,7 +99,7 @@ class HistorySection extends StatelessWidget {
else
asset = item.assetOfTransaction;
return GestureDetector(
return _historyRow(
onTap: () {
final page =
getIt.get<TransactionDetailsModal>(param1: transaction);
@@ -125,7 +134,7 @@ class HistorySection extends StatelessWidget {
final tradeFrom = trade.from;
final tradeTo = trade.to;
return GestureDetector(
return _historyRow(
onTap: () => Navigator.of(context)
.pushNamed(Routes.tradeDetails, arguments: trade),
child: HistoryTradeTile(
@@ -161,7 +170,7 @@ class HistorySection extends StatelessWidget {
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant)));
} else if (item is OrderListItem) {
return GestureDetector(
return _historyRow(
onTap: () => Navigator.of(context)
.pushNamed(Routes.orderDetails, arguments: item.order),
child: HistoryOrderTile(
@@ -176,7 +185,7 @@ class HistorySection extends StatelessWidget {
} else if (item is PayjoinTransactionListItem) {
final session = item.session;
return GestureDetector(
return _historyRow(
onTap: () => Navigator.of(context).pushNamed(
Routes.payjoinDetails,
arguments: [item.sessionId, item.transaction],
@@ -196,7 +205,7 @@ class HistorySection extends StatelessWidget {
} else if (item is AnonpayTransactionListItem) {
final transactionInfo = item.transaction;
return GestureDetector(
return _historyRow(
onTap: () => Navigator.of(context).pushNamed(
Routes.anonPayDetailsPage,
arguments: transactionInfo),
@@ -142,7 +142,8 @@ class HistoryTile extends StatelessWidget {
date: date,
amount: amount,
amountFiat: amountFiat,
leadingIcon: _getLeadingIcon(context),
// Decorative: `title` already reads out sent/received/pending.
leadingIcon: ExcludeSemantics(child: _getLeadingIcon(context)),
primaryTextColor: _getPrimaryTextColor(),
roundedTop: roundedTop,
roundedBottom: roundedBottom,
@@ -11,46 +11,53 @@ class HistoryTopBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: GestureDetector(
child: Semantics(
button: true,
label: S.of(context).history,
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular(18),
bottom: roundedBottom ? Radius.circular(18) : Radius.zero),
color: Theme.of(context).colorScheme.surfaceContainer),
child: ExcludeSemantics(
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 12,
),
child: Column(
spacing: 12,
children: [
SizedBox.shrink(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular(18),
bottom: roundedBottom ? Radius.circular(18) : Radius.zero),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 4,
horizontal: 12,
),
child: Column(
spacing: 12,
children: [
Text(S.of(context).history),
CakeImageWidget(
imageUrl: "assets/new-ui/arrow_right.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
)
SizedBox.shrink(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).history),
CakeImageWidget(
imageUrl: "assets/new-ui/arrow_right.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
)
],
),
if (!roundedBottom)
Container(
height: 1,
width: double.infinity,
color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175),
)
else
Container(height: 2)
],
),
if (!roundedBottom)
Container(
height: 1,
width: double.infinity,
color: Theme.of(context).colorScheme.outlineVariant.withAlpha(175),
)
else
Container(height: 2)
],
),
),
),
),
@@ -91,11 +91,14 @@ class BalanceCard extends StatelessWidget {
? ClipRSuperellipse(
borderRadius: BorderRadius.circular(borderRadius),
key: ValueKey(design.imagePath),
child: CakeImageWidget(
imageUrl: design.imagePath,
width: width,
height: height,
fit: BoxFit.fill,
// Purely decorative card artwork.
child: ExcludeSemantics(
child: CakeImageWidget(
imageUrl: design.imagePath,
width: width,
height: height,
fit: BoxFit.fill,
),
),
)
: const SizedBox.shrink(
@@ -146,9 +149,14 @@ class BalanceCard extends StatelessWidget {
AnimatedOpacity(
opacity: selected ? 0 : 1,
duration: textFadeDuration,
child: Text(
accountBalance,
style: TextStyle(color: design.colors.textColor, fontSize: 14),
// Opacity alone keeps the text readable by screen
// readers, so drop it while it is invisible.
child: ExcludeSemantics(
excluding: selected,
child: Text(
accountBalance,
style: TextStyle(color: design.colors.textColor, fontSize: 14),
),
),
),
],
@@ -156,40 +164,45 @@ class BalanceCard extends StatelessWidget {
AnimatedOpacity(
opacity: selected ? 1 : 0,
duration: textFadeDuration,
child: AnimatedSwitcher(
duration: designSwitchDuration,
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
// Only the selected card's balance is visible, so only it
// may be announced.
child: ExcludeSemantics(
excluding: !selected,
child: AnimatedSwitcher(
duration: designSwitchDuration,
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
child: Row(
key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
spacing: 8.0,
children: [
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context).style.copyWith(
color: design.colors.textColor,
fontSize: 28,
fontWeight: FontWeight.w500,
letterSpacing: -0.4),
child: Text(fiatFirst ? fiatBalance : balance),
),
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context).style.copyWith(
color: design.colors.textColorSecondary,
fontSize: 28,
fontWeight: FontWeight.w400,
letterSpacing: -0.4),
child: Text(resolvedAssetName),
),
],
);
},
child: Row(
key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
spacing: 8.0,
children: [
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context).style.copyWith(
color: design.colors.textColor,
fontSize: 28,
fontWeight: FontWeight.w500,
letterSpacing: -0.4),
child: Text(fiatFirst ? fiatBalance : balance),
),
AnimatedDefaultTextStyle(
duration: designSwitchDuration,
style: DefaultTextStyle.of(context).style.copyWith(
color: design.colors.textColorSecondary,
fontSize: 28,
fontWeight: FontWeight.w400,
letterSpacing: -0.4),
child: Text(resolvedAssetName),
),
],
),
),
),
),
@@ -250,7 +263,10 @@ class BalanceCard extends StatelessWidget {
switchInCurve: Curves.easeInOut,
switchOutCurve: Curves.easeInOut,
child: design.backgroundType == CardDesignBackgroundTypes.svgIcon
? _CornerSvgIcon(design: design, iconWidth: iconWidth)
// Purely decorative card artwork.
? ExcludeSemantics(
child: _CornerSvgIcon(design: design, iconWidth: iconWidth),
)
: const SizedBox.shrink(
key: ValueKey('svgIconOff'),
),
@@ -266,18 +282,28 @@ class BalanceCard extends StatelessWidget {
child: AnimatedOpacity(
duration: designSwitchDuration,
opacity: onCustomizeTapped == null ? 0 : 1,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onCustomizeTapped,
child: Container(
height: 40,
width: 40,
child: Center(
child: CakeImageWidget(
imageUrl: "assets/new-ui/3dots_vertical.svg",
alignment: Alignment.topRight,
colorFilter:
ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
// Faded out and inert: must not be a focusable phantom control.
child: ExcludeSemantics(
excluding: onCustomizeTapped == null,
child: Semantics(
button: true,
label: S.of(context).wallet_menu,
onTap: onCustomizeTapped,
child: GestureDetector(
excludeFromSemantics: true,
behavior: HitTestBehavior.opaque,
onTap: onCustomizeTapped,
child: Container(
height: 40,
width: 40,
child: Center(
child: CakeImageWidget(
imageUrl: "assets/new-ui/3dots_vertical.svg",
alignment: Alignment.topRight,
colorFilter:
ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
),
),
),
),
),
@@ -289,27 +315,34 @@ class BalanceCard extends StatelessWidget {
);
}
Widget getBalanceCardActionButton(BalanceCardAction action) => GestureDetector(
Widget getBalanceCardActionButton(BalanceCardAction action) => Semantics(
button: true,
label: action.label,
onTap: action.onTap,
child: Container(
decoration: BoxDecoration(
color: design.colors.backgroundImageColor.withAlpha(75),
borderRadius: BorderRadius.circular(10000000),
),
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(right: 6),
child: Text(
action.label,
style: TextStyle(color: design.colors.textColor, fontSize: 16),
),
child: ExcludeSemantics(
child: GestureDetector(
onTap: action.onTap,
child: Container(
decoration: BoxDecoration(
color: design.colors.backgroundImageColor.withAlpha(75),
borderRadius: BorderRadius.circular(10000000),
),
Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
],
margin: const EdgeInsets.only(right: 10),
padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(right: 6),
child: Text(
action.label,
style: TextStyle(color: design.colors.textColor, fontSize: 16),
),
),
Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
],
),
),
),
),
);
@@ -76,6 +76,32 @@ class _CardsViewState extends State<CardsView> {
final left = (parentWidth - effectiveCardWidth) / 2.0;
final isSelected = _selectedIndex == visualIndex;
final accounts = widget.accountListViewModel?.accounts;
final cardLabel = (accounts != null && realIndex < accounts.length)
? accounts[realIndex].label
: S.of(context).balance;
void onCardTap() {
// printV(visualIndex);
if (compactMode && visualIndex != 0) {
widget.onCompactModeBackgroundCardsTapped();
} else if (!compactMode) {
setState(() {
if (widget.accountListViewModel != null)
widget.accountListViewModel!.select(widget.accountListViewModel!.accounts[realIndex]);
_selectedIndex = visualIndex;
});
}
}
void onCardLongPress() {
if (_selectedIndex == visualIndex) {
widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
}
HapticFeedback.heavyImpact();
}
return AnimatedPositioned(
key: ValueKey("$visualIndex $realIndex"),
duration: animDuration,
@@ -86,124 +112,122 @@ class _CardsViewState extends State<CardsView> {
duration: animDuration,
curve: Curves.easeOut,
scale: scale,
child: GestureDetector(
onTap: () {
// printV(visualIndex);
if (compactMode && visualIndex != 0) {
widget.onCompactModeBackgroundCardsTapped();
} else if (!compactMode) {
setState(() {
if (widget.accountListViewModel != null)
widget.accountListViewModel!
.select(widget.accountListViewModel!.accounts[realIndex]);
_selectedIndex = visualIndex;
});
}
},
onLongPress: () {
if (_selectedIndex == visualIndex) {
widget.dashboardViewModel.balanceViewModel.switchBalanceValue();
}
;
HapticFeedback.heavyImpact();
},
child: Observer(builder: (_) {
if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
return Container();
}
final account = widget.accountListViewModel?.accounts[realIndex];
// The second balance should always be the lightning balance
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel
.getMainBalanceRecord(widget.lightningMode);
late final String walletBalance;
late final String walletFiatBalance;
if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
if (widget.dashboardViewModel.balanceViewModel.displayMode ==
BalanceDisplayMode.hiddenBalance) {
walletBalance = '●●●●●●';
walletFiatBalance = '●●●●●●';
} else {
walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
// The card is the tap target; the balances and the card's own buttons stay
// reachable as children of this node.
child: Semantics(
button: true,
selected: isSelected,
label: cardLabel,
hint: isSelected
? (widget.dashboardViewModel.balanceViewModel.displayMode ==
BalanceDisplayMode.hiddenBalance
? S.of(context).long_press_show_balance
: S.of(context).long_press_hide_balance)
: null,
onTap: onCardTap,
onLongPress: isSelected ? onCardLongPress : null,
child: GestureDetector(
excludeFromSemantics: true,
onTap: onCardTap,
onLongPress: onCardLongPress,
child: Observer(builder: (_) {
if (realIndex >= (widget.accountListViewModel?.accounts.length ?? 1)) {
return Container();
}
} else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
walletBalance = "";
walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
} else {
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
}
final account = widget.accountListViewModel?.accounts[realIndex];
// the card designs is empty if widget gets built before it loads.
// should get populated before user sees anything
final CardDesign cardDesign;
if (widget.dashboardViewModel.cardDesigns.isEmpty ||
realIndex >= widget.dashboardViewModel.cardDesigns.length)
cardDesign = CardDesign.genericDefault;
else if (widget.lightningMode)
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
else
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
// The second balance should always be the lightning balance
// printV(widget.dashboardViewModel.balanceViewModel.formattedBalances.first.availableBalance);
final walletBalanceRecord = widget.dashboardViewModel.balanceViewModel
.getMainBalanceRecord(widget.lightningMode);
final String accountName;
final String accountBalance;
if (account == null) {
accountName = "";
accountBalance = "";
} else {
accountName = account.label;
accountBalance = account.balance ?? "0.00";
}
late final String walletBalance;
late final String walletFiatBalance;
if (widget.dashboardViewModel.mwebEnabled && widget.dashboardViewModel.hasMweb) {
if (widget.dashboardViewModel.balanceViewModel.displayMode ==
BalanceDisplayMode.hiddenBalance) {
walletBalance = '●●●●●●';
walletFiatBalance = '●●●●●●';
} else {
walletBalance = walletBalanceRecord?.combinedAvailableBalance ?? "0";
walletFiatBalance = walletBalanceRecord?.combinedFiatAvailableBalance ?? "0.00";
}
} else if (widget.dashboardViewModel.balanceViewModel.showCombinedBalance) {
walletBalance = "";
walletFiatBalance = widget.dashboardViewModel.balanceViewModel.combinedFiatBalance;
} else {
walletBalance = walletBalanceRecord?.availableBalance ?? "0";
walletFiatBalance = walletBalanceRecord?.fiatAvailableBalance ?? "0.00";
}
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
? ""
: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
// the card designs is empty if widget gets built before it loads.
// should get populated before user sees anything
final CardDesign cardDesign;
if (widget.dashboardViewModel.cardDesigns.isEmpty ||
realIndex >= widget.dashboardViewModel.cardDesigns.length)
cardDesign = CardDesign.genericDefault;
else if (widget.lightningMode)
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex + 1];
else
cardDesign = widget.dashboardViewModel.cardDesigns[realIndex];
final List<BalanceCardAction> actions = widget.lightningMode
? [
BalanceCardAction(
label: S.current.bitcoin_lightning_deposit,
icon: Icons.arrow_downward,
onTap: depositToL2,
),
BalanceCardAction(
label: S.current.bitcoin_lightning_withdraw,
icon: Icons.arrow_upward,
onTap: withdrawFromL2,
)
]
: widget.dashboardViewModel.isEnabledTradeAction
? [
BalanceCardAction(
label: S.current.buy,
icon: Icons.arrow_forward_ios_rounded,
iconSize: 12,
onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage),
)
]
: [];
final String accountName;
final String accountBalance;
if (account == null) {
accountName = "";
accountBalance = "";
} else {
accountName = account.label;
accountBalance = account.balance ?? "0.00";
}
return BalanceCard(
width: effectiveCardWidth,
accountName: accountName,
accountBalance: accountBalance,
designSwitchDuration: Duration(milliseconds: 150),
assetName: assetName,
capitalizeAssetName: _shouldCapitalizeAssetName(),
balance: walletBalance,
fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ??
widget.dashboardViewModel.settingsStore.fiatCurrency.title,
fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
fiatBalance: walletFiatBalance,
selected: _selectedIndex == visualIndex,
onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
design: cardDesign,
actions: actions,
);
}),
final assetName = widget.dashboardViewModel.balanceViewModel.showCombinedBalance
? ""
: walletBalanceRecord?.formattedAssetTitle ?? assetTitleFallback;
final List<BalanceCardAction> actions = widget.lightningMode
? [
BalanceCardAction(
label: S.current.bitcoin_lightning_deposit,
icon: Icons.arrow_downward,
onTap: depositToL2,
),
BalanceCardAction(
label: S.current.bitcoin_lightning_withdraw,
icon: Icons.arrow_upward,
onTap: withdrawFromL2,
)
]
: widget.dashboardViewModel.isEnabledTradeAction
? [
BalanceCardAction(
label: S.current.buy,
icon: Icons.arrow_forward_ios_rounded,
iconSize: 12,
onTap: () => Navigator.of(context).pushNamed(Routes.buySellPage),
)
]
: [];
return BalanceCard(
width: effectiveCardWidth,
accountName: accountName,
accountBalance: accountBalance,
designSwitchDuration: Duration(milliseconds: 150),
assetName: assetName,
capitalizeAssetName: _shouldCapitalizeAssetName(),
balance: walletBalance,
fiatCurrencyTitle: walletBalanceRecord?.fiatCurrency?.title ??
widget.dashboardViewModel.settingsStore.fiatCurrency.title,
fiatFirst: widget.dashboardViewModel.balanceViewModel.showCombinedBalance,
fiatBalance: walletFiatBalance,
selected: _selectedIndex == visualIndex,
onCustomizeTapped: _selectedIndex == visualIndex ? widget.onCustomizeTapped : null,
design: cardDesign,
actions: actions,
);
}),
),
),
),
);
+51 -39
View File
@@ -21,51 +21,63 @@ class MwebAd extends StatelessWidget {
child: Column(
spacing: 12,
children: [
GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
child: Container(
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/settings_row_icons/mweb.svg",
width: 24,
height: 24,
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(
S.of(context).mweb_ad,
softWrap: true,
style: TextStyle(fontSize: 12),
MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(
onTap: () => Navigator.of(context).pushNamed(Routes.mwebSettings),
child: Container(
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: "assets/new-ui/settings_row_icons/mweb.svg",
width: 24,
height: 24,
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Text(
S.of(context).mweb_ad,
softWrap: true,
style: TextStyle(fontSize: 12),
),
),
),
Icon(
size: 16,
Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.primary,
)
],
),
Icon(
size: 16,
Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.primary,
)
],
),
),
),
),
),
GestureDetector(
onTap: () => dashboardViewModel.dismissMwebAd(false),
child: Text(
S.of(context).do_not_show_anymore,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary),
MergeSemantics(
child: Semantics(
button: true,
child: GestureDetector(
onTap: () => dashboardViewModel.dismissMwebAd(false),
child: Text(
S.of(context).do_not_show_anymore,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary),
),
),
),
)
],
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
@@ -27,10 +28,16 @@ class ChainIcon extends StatelessWidget {
AnimatedOpacity(
duration: Duration(milliseconds: 100),
opacity: done ? 0 : 1,
child: CircularProgressIndicator(
value: progress,
color: Color(0xFFFFB84E),
strokeWidth: 2,
// Faded out means "nothing to report", so it must leave the tree too.
child: ExcludeSemantics(
excluding: done,
child: CircularProgressIndicator(
value: progress,
color: Color(0xFFFFB84E),
strokeWidth: 2,
semanticsLabel: S.of(context).synchronizing,
semanticsValue: "${(progress * 100).round()}%",
),
),
),
AnimatedScale(
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -12,76 +13,84 @@ class LightningSwitcher extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
child: InkWell(
onTap: () {
HapticFeedback.mediumImpact();
onLightningSwitchPress();
},
child: Container(
decoration: ShapeDecoration(
shape: RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
color: Theme.of(context).colorScheme.surfaceContainer),
width: 70,
height: 36,
padding: EdgeInsets.symmetric(vertical: 2),
child: Stack(
children: [
AnimatedContainer(
alignment: Alignment.centerRight,
margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
duration: Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
width: 32,
height: 32,
// height: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
color: Theme.of(context).colorScheme.primary),
),
Container(
child: Row(
spacing: 2.0,
children: [
SizedBox(),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
key: ValueKey(lightningMode),
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
BlendMode.srcIn,
),
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/switcher-lightning.svg',
key: ValueKey(lightningMode),
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
],
// One toggle node: the knob position and the coloured glyphs are the only
// visual cue for which mode is active.
return Semantics(
button: true,
toggled: lightningMode,
label: S.of(context).lightning_mode,
child: SizedBox(
child: InkWell(
onTap: () {
HapticFeedback.mediumImpact();
onLightningSwitchPress();
},
child: Container(
decoration: ShapeDecoration(
shape:
RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
color: Theme.of(context).colorScheme.surfaceContainer),
width: 70,
height: 36,
padding: EdgeInsets.symmetric(vertical: 2),
child: Stack(
children: [
AnimatedContainer(
alignment: Alignment.centerRight,
margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
duration: Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
width: 32,
height: 32,
// height: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(9999990.0)),
color: Theme.of(context).colorScheme.primary),
),
),
],
Container(
child: Row(
spacing: 2.0,
children: [
SizedBox(),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/switcher-bitcoin.svg',
key: ValueKey(lightningMode),
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainer,
BlendMode.srcIn,
),
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/switcher-lightning.svg',
key: ValueKey(lightningMode),
width: 32,
height: 32,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
],
),
),
],
),
),
),
),
@@ -1,5 +1,6 @@
import 'package:cake_wallet/core/sync_status_title.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
@@ -47,65 +48,55 @@ class SyncBar extends StatelessWidget {
child: Stack(
alignment: Alignment.centerLeft,
children: [
if (!_showFullBar())
Row(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
if (dashboardViewModel.isTorEnabled)
CakeImageWidget(
imageUrl: "assets/new-ui/tor.svg",
width: 20,
height: 20,
),
if (_showDot()) PulsingDot(),
],
),
if (!_showFullBar()) _buildCompactBar(context),
if (_showFullBar())
GestureDetector(
onTap: () {
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
child: Material(
child: getIt.get<ManageNodesPage>(param1: false),
)));
},
child: AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: Container(
key: ValueKey(status.runtimeType),
height: 36,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999),
border: _getBorder(context, status.runtimeType),
color: _getBackgroundColor(context, status.runtimeType),
),
child: Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
if (icon != null) icon,
// if (dashboardViewModel.silentPaymentsScanningActive &&
// progressStatuses.contains(status.runtimeType)) ...[
// Text(
// "${(status.progress() * 100).toInt()}%",
// style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
// ),
// Text(
// "·",
// style: TextStyle(fontSize: 12),
// )
// ],
Text(
syncStatusTitle(
status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
style: _getTextStyle(context, status.runtimeType),
// A single node: the localized status text (plus any active
// Tor/MWEB/Silent Payments badge) is the label, and the hint says
// where tapping leads. Everything inside is redundant with it.
Semantics(
button: true,
label: _statusSemanticsLabel(context, status),
hint: S.of(context).manage_nodes,
onTap: () => _openNodeManagement(context),
child: ExcludeSemantics(
child: GestureDetector(
onTap: () => _openNodeManagement(context),
child: AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: Container(
key: ValueKey(status.runtimeType),
height: 36,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999),
border: _getBorder(context, status.runtimeType),
color: _getBackgroundColor(context, status.runtimeType),
),
],
child: Row(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
if (icon != null) icon,
// if (dashboardViewModel.silentPaymentsScanningActive &&
// progressStatuses.contains(status.runtimeType)) ...[
// Text(
// "${(status.progress() * 100).toInt()}%",
// style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)),
// ),
// Text(
// "·",
// style: TextStyle(fontSize: 12),
// )
// ],
Text(
syncStatusTitle(status,
dashboardViewModel.settingsStore.syncStatusDisplayMode),
style: _getTextStyle(context, status.runtimeType),
),
],
),
),
),
),
),
@@ -117,6 +108,55 @@ class SyncBar extends StatelessWidget {
);
}
void _openNodeManagement(BuildContext context) =>
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
child: Material(
child: getIt.get<ManageNodesPage>(param1: false),
)));
/// Compact mode shows sync state with a pulsing dot (and a Tor glyph) only, so
/// the whole row needs a text equivalent.
Widget _buildCompactBar(BuildContext context) {
final row = Row(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
if (dashboardViewModel.isTorEnabled)
CakeImageWidget(
imageUrl: "assets/new-ui/tor.svg",
width: 20,
height: 20,
),
if (_showDot()) PulsingDot(),
],
);
final label = _joinLabels([
if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
if (_showDot()) S.of(context).synchronizing,
]);
if (label.isEmpty) return row;
return Semantics(label: label, child: ExcludeSemantics(child: row));
}
String _statusSemanticsLabel(BuildContext context, SyncStatus status) {
final isFailure = failStatuses.contains(status.runtimeType);
return _joinLabels([
syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
if (!isFailure && dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
if (!isFailure && dashboardViewModel.hasMweb) S.of(context).litecoin_mweb,
if (!isFailure && dashboardViewModel.hasSilentPayments) S.of(context).silent_payments,
]);
}
String _joinLabels(List<String> parts) => parts.where((part) => part.isNotEmpty).join(", ");
Color? _getBackgroundColor(BuildContext context, Type status) {
if (failStatuses.contains(status)) {
return Theme.of(context).colorScheme.errorContainer.withAlpha(64);
+27 -13
View File
@@ -21,15 +21,22 @@ class WalletInfoBar extends StatelessWidget {
final bool hasCustomize;
final VoidCallback onCustomizeButtonTap;
void _openAccountCustomizer() {
if (hasCustomize) {
onCustomizeButtonTap();
HapticFeedback.mediumImpact();
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (hasCustomize) {
onCustomizeButtonTap();
HapticFeedback.mediumImpact();
}
},
// The row, the hardware-wallet glyph and the inner accounts button are one
// control for a screen reader: a single labeled node opening the customizer.
final semanticsLabel =
hardwareWalletType == null ? name : "$name, ${S.of(context).hardware_wallet}";
final row = GestureDetector(
onTap: _openAccountCustomizer,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
@@ -70,12 +77,7 @@ class WalletInfoBar extends StatelessWidget {
SizedBox(width: 8),
ModernButton.svg(
size: 24,
onPressed: () {
if (hasCustomize) {
onCustomizeButtonTap();
HapticFeedback.mediumImpact();
}
},
onPressed: _openAccountCustomizer,
svgPath: "assets/new-ui/icon-accounts.svg",
semanticLabel: S.of(context).wallet_accounts,
)
@@ -83,6 +85,18 @@ class WalletInfoBar extends StatelessWidget {
],
),
);
if (!hasCustomize) {
return Semantics(label: semanticsLabel, child: ExcludeSemantics(child: row));
}
return Semantics(
button: true,
label: semanticsLabel,
hint: S.of(context).wallet_accounts,
onTap: _openAccountCustomizer,
child: ExcludeSemantics(child: row),
);
}
String? get hardwareWalletIcon {
+33 -24
View File
@@ -93,33 +93,42 @@ class _LineTabSwitcherState extends State<LineTabSwitcher> {
children: widget.tabs.map((item) {
final index = widget.tabs.indexOf(item);
return GestureDetector(
onTap: () {
widget.onTabChange(index);
},
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedDefaultTextStyle(
duration: Duration(milliseconds: 150),
style: DefaultTextStyle.of(context).style.copyWith(
inherit: true,
fontSize: 16,
fontWeight: FontWeight.w500,
color: widget.selectedTab == index
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurfaceVariant,
// One tab = one node: the visible label merges into a button node
// that also carries the selected state of this tab.
return MergeSemantics(
child: Semantics(
button: true,
selected: widget.selectedTab == index,
inMutuallyExclusiveGroup: true,
child: GestureDetector(
onTap: () {
widget.onTabChange(index);
},
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedDefaultTextStyle(
duration: Duration(milliseconds: 150),
style: DefaultTextStyle.of(context).style.copyWith(
inherit: true,
fontSize: 16,
fontWeight: FontWeight.w500,
color: widget.selectedTab == index
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurfaceVariant,
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: itemPadding / 2),
child: Text(
item,
key: textWidgetKeys[index],
),
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: itemPadding / 2),
child: Text(
item,
key: textWidgetKeys[index],
),
),
],
),
],
),
),
);
}).toList())),
+31 -24
View File
@@ -60,31 +60,38 @@ class _LongPressMenuState extends State<LongPressMenu> {
final color = item.color ?? Theme.of(context).colorScheme.onSurface;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: item.onSelected,
child: Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 12,
),
child: Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 8,
children: [
CakeImageWidget(
imageUrl: item.iconPath,
height: 20,
width: 20,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
onTap: item.onSelected,
child: Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 12,
bottom: 12,
),
child: Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 8,
children: [
ExcludeSemantics(
child: CakeImageWidget(
imageUrl: item.iconPath,
height: 20,
width: 20,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
),
),
Text(item.label,
style: TextStyle(
color: color, fontSize: 14, fontWeight: FontWeight.w500)),
],
),
Text(item.label,
style: TextStyle(
color: color, fontSize: 14, fontWeight: FontWeight.w500)),
],
),
),
),
),
@@ -187,50 +187,56 @@ class _NEWNewMainNavBarState extends State<NewMainNavBar> {
? iconHorizontalPadding / 100
: 0),
curve: Curves.easeOutCubic,
child: InkWell(
splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
borderRadius: BorderRadius.circular(pillBorderRadius),
onTap: () => _onItemTap(i),
child: AnimatedContainer(
duration:
_firstFrame ? Duration.zero : inactiveIconMoveDuration,
curve: Curves.easeOutCubic,
width: i == widget.selectedIndex ? pillWidth : iconBoxWidth,
alignment: Alignment.center,
child: AnimatedAlign(
duration: inactiveIconFadeDuration,
child: Semantics(
button: true,
selected: i == widget.selectedIndex,
inMutuallyExclusiveGroup: true,
label: visibleActions[i].name(context),
child: InkWell(
splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
borderRadius: BorderRadius.circular(pillBorderRadius),
onTap: () => _onItemTap(i),
child: AnimatedContainer(
duration:
_firstFrame ? Duration.zero : inactiveIconMoveDuration,
curve: Curves.easeOutCubic,
width: i == widget.selectedIndex ? pillWidth : iconBoxWidth,
alignment: Alignment.center,
child: AnimatedScale(
duration: inactiveIconAppearDuration,
child: AnimatedAlign(
duration: inactiveIconFadeDuration,
curve: Curves.easeOutCubic,
scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
child: TweenAnimationBuilder<Color?>(
tween: ColorTween(
begin: (i == widget.selectedIndex)
? inactiveColor
: activeColor,
end: (i == widget.selectedIndex)
? activeColor
: inactiveColor,
),
duration: iconColorChangeDuration,
builder: (context, value, child) {
return Container(
height: NewMainNavBar.barHeight,
child: CakeImageWidget(
imageUrl: visibleActions[i].image,
width: iconWidth,
height: iconHeight,
//fit: BoxFit.scaleDown,
colorFilter: ColorFilter.mode(
value ?? inactiveColor,
BlendMode.srcIn,
alignment: Alignment.center,
child: AnimatedScale(
duration: inactiveIconAppearDuration,
curve: Curves.easeOutCubic,
scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
child: TweenAnimationBuilder<Color?>(
tween: ColorTween(
begin: (i == widget.selectedIndex)
? inactiveColor
: activeColor,
end: (i == widget.selectedIndex)
? activeColor
: inactiveColor,
),
duration: iconColorChangeDuration,
builder: (context, value, child) {
return Container(
height: NewMainNavBar.barHeight,
child: CakeImageWidget(
imageUrl: visibleActions[i].image,
width: iconWidth,
height: iconHeight,
//fit: BoxFit.scaleDown,
colorFilter: ColorFilter.mode(
value ?? inactiveColor,
BlendMode.srcIn,
),
),
),
);
}),
);
}),
),
),
),
),
@@ -306,11 +312,15 @@ class AnimatedPill extends StatelessWidget {
children: [
Padding(
padding: EdgeInsets.only(left: pillIconWidth + 2),
child: Text(
currentAction.name(context),
style: pillTextStyle.copyWith(color: contentColor),
overflow: TextOverflow.fade,
softWrap: false,
// The selected tab's InkWell already announces this name, so the
// pill text must not become a second stop for screen readers.
child: ExcludeSemantics(
child: Text(
currentAction.name(context),
style: pillTextStyle.copyWith(color: contentColor),
overflow: TextOverflow.fade,
softWrap: false,
),
),
),
],
+4
View File
@@ -528,6 +528,7 @@
"got_it": "Got it",
"gross_balance": "Gross Balance",
"group_by_type": "Group by type",
"hardware_wallet": "Hardware wallet",
"haven_app": "Haven by Cake Wallet",
"haven_app_wallet_text": "Awesome wallet for Haven",
"help": "help",
@@ -588,6 +589,7 @@
"lightning_deposit_desc": "When you deposit to Lightning, you are swapping your on-chain Bitcoin from this wallet to your Lightning account.",
"lightning_deposit_disclaimer": "The new Lightning balance will not be available until the Bitcoin transaction is fully confirmed.",
"lightning_external_disclaimer": "Don't close this page until you send the funds, or else the deposit won't proceed.",
"lightning_mode": "Lightning mode",
"lightning_username_desc": "Easily receive Lightning payments with a Lightning username by choosing a custom name or using a randomly generated one below.",
"lightning_username_desc_completed": "You can use this username to receive payments from any Lightning wallet.",
"lightning_username_setup_later": "If you skip now, you can set this up later in the Settings.",
@@ -618,6 +620,7 @@
"login": "Login",
"logout": "Logout",
"long_press_edit_address": "Long press to edit address",
"long_press_hide_balance": "Long press card to hide balance",
"long_press_show_balance": "Long press card to show balance",
"low_fee": "Low fee",
"low_fee_alert": "You currently are using a low network fee priority. This could cause long waits, different rates, or canceled trades. We recommend setting a higher fee for a better experience.",
@@ -726,6 +729,7 @@
"onramper_option_description": "Quickly buy crypto with many payment methods. Available in most countries. Spreads and fees vary.",
"open_gift_card": "Open Gift Card",
"open_wallet": "Open Wallet",
"opens_externally": "Opens externally",
"optional_description": "Optional description",
"optional_email_hint": "Optional payee notification email",
"optional_name": "Optional recipient name",