CW-1468 Home Screen Top Bar Layout (#3576)

* Improve homescreen layout and account access

* Add homescreen layout regression tests

* address feedback
This commit is contained in:
Patrick
2026-09-12 14:20:16 +02:00
committed by GitHub
parent bfae2191b0
commit 5b0f0fcd24
17 changed files with 1259 additions and 335 deletions
+7
View File
@@ -602,6 +602,13 @@ Future<void> setup({
getIt.registerFactory<AccountCreationModal>(() => AccountCreationModal(
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>()));
getIt.registerFactoryParam<AccountCustomizer, DashboardViewModel, void>(
(dashboardViewModel, _) => AccountCustomizer(
accountListViewModel: getIt.get<MoneroAccountListViewModel>(),
dashboardViewModel: dashboardViewModel,
),
);
getIt.registerFactory<LightningUsernameBloc>(
() => LightningUsernameBloc(getIt.get<AppStore>().wallet!));
+2 -8
View File
@@ -1,7 +1,6 @@
import 'dart:ui';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/new-ui/pages/card_customizer.dart';
import 'package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
@@ -24,7 +23,6 @@ import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class AccountCustomizerListItem {
@@ -38,13 +36,9 @@ class AccountCustomizerListItem {
class AccountCustomizer extends StatefulWidget {
const AccountCustomizer(
{super.key,
required this.accountListViewModel,
required this.accountEditOrCreateViewModel,
required this.dashboardViewModel});
{super.key, required this.accountListViewModel, required this.dashboardViewModel});
final MoneroAccountListViewModel accountListViewModel;
final MoneroAccountEditOrCreateViewModel accountEditOrCreateViewModel;
final DashboardViewModel dashboardViewModel;
@override
@@ -62,7 +56,7 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
loadCards();
final activeId = monero!.getCurrentAccount(widget.dashboardViewModel.wallet).id;
final activeId = widget.accountListViewModel.selected.id;
for (int i = 0; i < _items.length - 1; i++) {
if (_items[i].accountListItem.id == activeId) {
final lastIndex = _items.length - 1;
+77 -66
View File
@@ -14,11 +14,9 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/cards/cards_view.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart';
import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -115,6 +113,7 @@ class _NewHomePageState extends State<NewHomePage> {
spacing: 24.0,
children: [
TopBar(
key: ValueKey(widget.dashboardViewModel.wallet.id),
dashboardViewModel: widget.dashboardViewModel,
lightningMode: _lightningMode,
onLightningSwitchPress: () {
@@ -122,8 +121,8 @@ class _NewHomePageState extends State<NewHomePage> {
_lightningMode = !_lightningMode;
});
},
onSettingsButtonPress: () {
CupertinoScaffold.showCupertinoModalBottomSheet(
onSettingsButtonPress: () async {
await CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
@@ -133,70 +132,77 @@ class _NewHomePageState extends State<NewHomePage> {
authService: getIt.get<AuthService>(),
))),
);
if (!mounted) {
return;
}
_setAccountViewModel();
await widget.dashboardViewModel.loadCardDesigns();
if (mounted) {
setState(() {});
}
},
),
Observer(
builder: (_) => WalletInfoBar(
lightningMode: _lightningMode,
hardwareWalletType: widget.dashboardViewModel.wallet.hardwareWalletType,
name: widget.dashboardViewModel.wallet.name,
hasCustomize: accountListViewModel != null,
onCustomizeButtonTap: openAccountCustomizer),
),
Column(
spacing: 20,
children: [
Observer(
builder: (_) => CardsView(
key: ValueKey(widget.dashboardViewModel.wallet.name),
onCustomizeTapped: openCardCustomizer,
dashboardViewModel: widget.dashboardViewModel,
accountListViewModel: accountListViewModel,
onCompactModeBackgroundCardsTapped: openAccountCustomizer,
lightningMode: _lightningMode,
),
),
Observer(builder: (_) {
return AnimatedSize(
duration: Duration(milliseconds: 150),
curve: Curves.easeInOutCubic,
child: (widget.dashboardViewModel.shouldShowBalanceHiddenMessage)
? Column(
children: [
SizedBox(
height: 12,
width: double.infinity,
),
Text(
S.of(context).long_press_show_balance,
style: TextStyle(
color:
Theme.of(context).colorScheme.onSurfaceVariant),
)
],
)
: SizedBox(width: double.infinity),
);
}),
UnconfirmedBalanceWidget(
dashboardViewModel: widget.dashboardViewModel,
),
],
),
Observer(
builder: (_) {
return Column(
Column(
children: [
CoinActionRow(
lightningMode: _lightningMode,
showSwap: widget.dashboardViewModel.isEnabledSwapAction,
walletType: widget.dashboardViewModel.wallet.type,
Observer(
builder: (_) => CardsView(
key: ValueKey(widget.dashboardViewModel.wallet.name),
onCustomizeTapped: openCardCustomizer,
dashboardViewModel: widget.dashboardViewModel,
accountListViewModel: accountListViewModel,
onCompactModeBackgroundCardsTapped: openAccountCustomizer,
lightningMode: _lightningMode,
),
),
MwebAd(
Observer(builder: (_) {
return AnimatedSize(
duration: Duration(milliseconds: 150),
curve: Curves.easeInOutCubic,
child: (widget.dashboardViewModel.shouldShowBalanceHiddenMessage)
? Column(
children: [
SizedBox(
height: 12,
width: double.infinity,
),
Text(
S.of(context).long_press_show_balance,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant),
)
],
)
: SizedBox(width: double.infinity),
);
}),
UnconfirmedBalanceWidget(
dashboardViewModel: widget.dashboardViewModel,
),
],
);
},
),
Observer(
builder: (_) {
return Column(
children: [
CoinActionRow(
lightningMode: _lightningMode,
showSwap: widget.dashboardViewModel.isEnabledSwapAction,
walletType: widget.dashboardViewModel.wallet.type,
),
MwebAd(
dashboardViewModel: widget.dashboardViewModel,
),
],
);
},
),
],
),
],
),
@@ -234,7 +240,12 @@ class _NewHomePageState extends State<NewHomePage> {
);
}
void openAccountCustomizer() async {
Future<void> openAccountCustomizer() async {
final accountList = accountListViewModel;
if (accountList == null) {
return;
}
await CupertinoScaffold.showCupertinoModalBottomSheet(
barrierColor: Colors.black.withAlpha(60),
context: context,
@@ -243,15 +254,15 @@ class _NewHomePageState extends State<NewHomePage> {
parentContext: context,
heightMode: ModalHeightModes.fullScreen,
rootPage: Material(
child: AccountCustomizer(
accountListViewModel: accountListViewModel!,
accountEditOrCreateViewModel: getIt.get<MoneroAccountEditOrCreateViewModel>(),
dashboardViewModel: widget.dashboardViewModel,
)),
child: AccountCustomizer(
accountListViewModel: accountList,
dashboardViewModel: widget.dashboardViewModel,
),
),
);
},
);
widget.dashboardViewModel.loadCardDesigns();
await widget.dashboardViewModel.loadCardDesigns();
}
void openCardCustomizer() async {
+16 -1
View File
@@ -25,6 +25,8 @@ bool _hasMweb(DashboardViewModel vm) => vm.hasMweb;
bool _hasWalletConnect(DashboardViewModel vm) => vm.hasWalletConnect;
bool _hasAccounts(DashboardViewModel vm) => vm.balanceViewModel.hasAccounts;
bool _requiresKeyImageSync(DashboardViewModel vm) =>
vm.wallet.type == WalletType.monero &&
[HardwareWalletType.cupcake, HardwareWalletType.trezor].contains(vm.wallet.hardwareWalletType);
@@ -38,12 +40,14 @@ class SettingsListItem {
this.use2fa = _falseFunc,
this.condition = _trueFunc,
this.routeArgs,
this.routeArgsBuilder,
});
final String iconPath;
final String title;
final String route;
final Object? routeArgs;
final Object? Function(DashboardViewModel)? routeArgsBuilder;
final bool requireAuth;
final bool Function(DashboardViewModel) use2fa;
final bool Function(DashboardViewModel) condition;
@@ -58,6 +62,13 @@ class SettingsSectionData {
static SettingsSectionData walletSettings =
SettingsSectionData(S.current.wallet_settings, "assets/new-ui/wallet-setting.svg", [
SettingsListItem(
"assets/new-ui/settings_row_icons/accounts.svg",
S.current.accounts,
Routes.accountCustomizer,
condition: _hasAccounts,
routeArgsBuilder: (vm) => vm,
),
SettingsListItem(
"assets/new-ui/settings_row_icons/nodes.svg", S.current.nodes, Routes.manageNodes),
SettingsListItem(
@@ -148,7 +159,11 @@ class SettingsMainPage extends StatelessWidget {
conditionToDetermineIfToUse2FA: item.use2fa(dashboardViewModel),
route: item.route);
} else {
Navigator.of(context).pushNamed(item.route, arguments: item.routeArgs);
Navigator.of(context).pushNamed(
item.route,
arguments:
item.routeArgsBuilder?.call(dashboardViewModel) ?? item.routeArgs,
);
}
}
})
@@ -26,7 +26,12 @@ class AssetsTopBar extends StatelessWidget {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 32.0, bottom: 0.0, left: 12.0, right: 18.0),
padding: const EdgeInsets.only(
top: 20,
bottom: 0,
left: 12,
right: 18,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -20,7 +20,7 @@ class HistoryTopBar extends StatelessWidget {
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 24),
padding: const EdgeInsets.only(left: 16, right: 16, top: 20),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
@@ -30,64 +30,59 @@ class LightningSwitcher extends StatelessWidget {
shape:
RoundedSuperellipseBorder(borderRadius: BorderRadiusGeometry.circular(900.0)),
color: Theme.of(context).colorScheme.surfaceContainer),
width: 70,
width: 63,
height: 36,
padding: EdgeInsets.symmetric(vertical: 2),
padding: const EdgeInsets.all(4.5),
child: Stack(
children: [
AnimatedContainer(
alignment: Alignment.centerRight,
margin: EdgeInsets.only(left: lightningMode ? 36 : 2),
duration: Duration(milliseconds: 250),
margin: EdgeInsets.only(left: lightningMode ? 27 : 0),
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
width: 32,
height: 32,
// height: double.infinity,
width: 27,
height: 27,
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,
),
Row(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: "assets/new-ui/switcher-bitcoin.svg",
key: ValueKey(lightningMode),
width: 27,
height: 27,
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,
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: CakeImageWidget(
imageUrl: "assets/new-ui/switcher-lightning.svg",
key: ValueKey(lightningMode),
width: 27,
height: 27,
colorFilter: ColorFilter.mode(
lightningMode
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
],
),
),
],
),
],
),
@@ -3,6 +3,7 @@ import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import "package:cake_wallet/themes/core/theme_extension.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
import "package:flutter/cupertino.dart";
@@ -11,11 +12,11 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class SyncBar extends StatelessWidget {
SyncBar({
super.key,
const SyncBar({
required this.dashboardViewModel,
required this.isSyncHeavy,
required this.showSyncedMessage,
super.key,
});
final DashboardViewModel dashboardViewModel;
@@ -46,106 +47,126 @@ class SyncBar extends StatelessWidget {
@override
Widget build(BuildContext context) => Observer(
builder: (_) {
final status = dashboardViewModel.status;
final Widget? icon = _getIcon(context, status.runtimeType);
builder: (_) {
final status = dashboardViewModel.status;
if (!_showFullBar) {
return _buildCompactBar(context);
}
return Expanded(
child: Stack(
alignment: Alignment.centerLeft,
children: [
if (!_showFullBar()) _buildCompactBar(context),
if (_showFullBar())
// 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),
final Widget? icon = _getIcon(context, status.runtimeType);
final statusTitle = _statusTitle(context, status);
final silentPaymentsProgress = _silentPaymentsProgress(status);
final barHeight = silentPaymentsProgress == null ? 36.0 : 40.0;
// 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.
return Semantics(
button: true,
label: _statusSemanticsLabel(context, status, statusTitle),
value: silentPaymentsProgress,
hint: S.of(context).manage_nodes,
onTap: () => _openNodeManagement(context),
child: ExcludeSemantics(
child: GestureDetector(
onTap: () => _openNodeManagement(context),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
child: OverflowBox(
key: ValueKey(status.runtimeType),
alignment: Alignment.center,
minHeight: barHeight,
maxHeight: barHeight,
child: Container(
height: barHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999),
border: _getBorder(context, status.runtimeType),
color: _getBackgroundColor(context, status.runtimeType),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
spacing: 8,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
if (icon != null) icon,
if (silentPaymentsProgress != null) ...[
Text(
silentPaymentsProgress,
style: TextStyle(
fontSize: 12,
color: context.customColors.warningOutlineColor,
),
],
),
Text(
"·",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
Flexible(
child: Text(
statusTitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _getTextStyle(context, status.runtimeType),
),
),
),
],
),
),
),
),
],
),
);
},
);
),
),
);
},
);
void _openNodeManagement(BuildContext context) =>
CupertinoScaffold.showCupertinoModalBottomSheet(
context: context,
barrierColor: Colors.black.withAlpha(85),
builder: (context) => FractionallySizedBox(
child: Material(
child: getIt.get<ManageNodesPage>(param1: false),
)));
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,
mainAxisAlignment: MainAxisAlignment.start,
spacing: 6,
children: [
if (dashboardViewModel.isTorEnabled)
CakeImageWidget(
const CakeImageWidget(
imageUrl: "assets/new-ui/tor.svg",
width: 20,
height: 20,
),
if (_showDot()) const CupertinoActivityIndicator(radius: 8),
if(_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18),
],
if (_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18),
],
);
final label = _joinLabels([
if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection,
if (_showDot()) S.of(context).synchronizing,
if (_showLightSyncCheck()) S.of(context).sync_synced,
]);
if (label.isEmpty) return row;
if (label.isEmpty) {
return row;
}
return Semantics(label: label, child: ExcludeSemantics(child: row));
}
@@ -153,11 +174,17 @@ class SyncBar extends StatelessWidget {
bool get _isShowingSyncedMessage =>
showSyncedMessage && dashboardViewModel.status.runtimeType == SyncedSyncStatus;
String _statusSemanticsLabel(BuildContext context, SyncStatus status) {
String _statusTitle(BuildContext context, SyncStatus status) {
final title = syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode);
return title.isNotEmpty ? title : S.of(context).synchronizing;
}
String _statusSemanticsLabel(BuildContext context, SyncStatus status, String statusTitle) {
final isFailure = failStatuses.contains(status.runtimeType);
return _joinLabels([
syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode),
statusTitle,
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,
@@ -166,6 +193,16 @@ class SyncBar extends StatelessWidget {
String _joinLabels(List<String> parts) => parts.where((part) => part.isNotEmpty).join(", ");
String? _silentPaymentsProgress(SyncStatus status) {
if (!dashboardViewModel.hasSilentPayments ||
!dashboardViewModel.silentPaymentsScanningActive ||
status is! SyncingSyncStatus) {
return null;
}
return "${(status.progress() * 100).toInt()}%";
}
Color? _getBackgroundColor(BuildContext context, Type status) {
if (failStatuses.contains(status)) {
return Theme.of(context).colorScheme.errorContainer.withAlpha(64);
@@ -192,14 +229,12 @@ class SyncBar extends StatelessWidget {
color = Theme.of(context).colorScheme.onSurfaceVariant;
}
return TextStyle(
fontSize: 12, fontWeight: FontWeight.w400, color: color);
return TextStyle(fontSize: 12, fontWeight: FontWeight.w400, color: color);
}
Widget? _getIcon(BuildContext context, Type status) {
if(status == SyncedSyncStatus) {
return Icon(Icons.check, color: syncedColor, size: 12);
if (status == SyncedSyncStatus) {
return const Icon(Icons.check, color: syncedColor, size: 12);
}
if (status == LostConnectionSyncStatus) {
@@ -219,22 +254,35 @@ class SyncBar extends StatelessWidget {
final List<Widget> children = [];
if (dashboardViewModel.isTorEnabled) {
children.add(CakeImageWidget(
children.add(
const CakeImageWidget(
imageUrl: "assets/new-ui/tor_sync.svg",
colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn)));
colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn),
),
);
}
if (dashboardViewModel.hasMweb) {
children.add(CakeImageWidget(
imageUrl: "assets/new-ui/mweb_sync.svg",
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
));
children.add(
CakeImageWidget(
imageUrl: "assets/new-ui/mweb_sync.svg",
colorFilter:
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
);
}
if (dashboardViewModel.hasSilentPayments) {
children.add(CakeImageWidget(
imageUrl: "assets/new-ui/silent_sync.svg",
colorFilter: ColorFilter.mode(Color(0xFFEFBA5E), BlendMode.srcIn),
));
children.add(
CakeImageWidget(
imageUrl: "assets/new-ui/silent_sync.svg",
width: 16,
height: 16,
colorFilter: ColorFilter.mode(context.customColors.warningOutlineColor, BlendMode.srcIn),
),
);
}
if (children.isEmpty) {
return null;
}
return Row(
@@ -243,15 +291,16 @@ class SyncBar extends StatelessWidget {
);
}
bool _showFullBar() {
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus)
bool get _showFullBar {
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus) {
return isSyncHeavy && _isShowingSyncedMessage;
}
return isSyncHeavy || failStatuses.contains(dashboardViewModel.status.runtimeType);
}
bool _showDot() =>
!isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType);
bool _showLightSyncCheck() =>
!isSyncHeavy && _isShowingSyncedMessage;
bool _showLightSyncCheck() => !isSyncHeavy && _isShowingSyncedMessage;
}
@@ -5,6 +5,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/chain_icon.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_switcher.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart';
import "package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart";
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
@@ -15,11 +16,11 @@ import 'package:mobx/mobx.dart';
class TopBar extends StatefulWidget {
const TopBar({
super.key,
required this.lightningMode,
required this.onLightningSwitchPress,
required this.dashboardViewModel,
required this.onSettingsButtonPress,
super.key,
});
final bool lightningMode;
@@ -36,25 +37,59 @@ class _TopBarState extends State<TopBar> {
bool showSyncedMessage = false;
Timer? syncedMessageTimer;
late final ReactionDisposer? _statusReactionDisposer;
ReactionDisposer? _statusReactionDisposer;
bool get replacesWalletName {
final status = widget.dashboardViewModel.status.runtimeType;
if (status == SyncedSyncStatus) {
return showSyncedMessage;
}
return widget.dashboardViewModel.isSyncHeavy ||
SyncBar.progressStatuses.contains(status) ||
SyncBar.failStatuses.contains(status);
}
@override
void initState() {
super.initState();
_bindStatusReaction();
}
@override
void didUpdateWidget(covariant TopBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (identical(oldWidget.dashboardViewModel, widget.dashboardViewModel)) {
return;
}
syncedMessageTimer?.cancel();
syncedMessageTimer = null;
showSyncedMessage = false;
_statusReactionDisposer?.call();
_bindStatusReaction();
}
void _bindStatusReaction() {
_statusReactionDisposer = reaction(
(_) => widget.dashboardViewModel.status.runtimeType,
(status) {
syncedMessageTimer?.cancel();
syncedMessageTimer = null;
if (status == SyncedSyncStatus) {
if(mounted) {
if (mounted) {
setState(() => showSyncedMessage = true);
}
syncedMessageTimer =
Timer(syncedMessageDuration, () => setState(() => showSyncedMessage = false));
syncedMessageTimer = Timer(syncedMessageDuration, () {
syncedMessageTimer = null;
if (mounted) {
setState(() => showSyncedMessage = false);
}
});
} else {
if(mounted) {
if (mounted) {
setState(() => showSyncedMessage = false);
}
}
@@ -65,51 +100,90 @@ class _TopBarState extends State<TopBar> {
@override
void dispose() {
syncedMessageTimer?.cancel();
_statusReactionDisposer?.reaction.dispose();
syncedMessageTimer = null;
_statusReactionDisposer?.call();
super.dispose();
}
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.only(
bottom: 10, left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
child: Observer(
builder: (_) => Row(
spacing: 12,
children: [
(widget.dashboardViewModel.hasLightning)
? LightningSwitcher(
lightningMode: widget.lightningMode,
onLightningSwitchPress: widget.onLightningSwitchPress,
)
: ChainIcon(
iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage),
SyncBar(
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
ModernButton.svg(
iconColor: Theme.of(context).colorScheme.primary,
size: 36,
onPressed: () {
HapticFeedback.mediumImpact();
widget.onSettingsButtonPress();
},
svgPath: "assets/new-ui/top-settings.svg",
semanticLabel: S.of(context).settings_title,
),
],
padding: EdgeInsets.only(left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
child: Observer(
builder: (_) => Row(
spacing: 12,
children: [
widget.dashboardViewModel.hasLightning
? LightningSwitcher(
lightningMode: widget.lightningMode,
onLightningSwitchPress: widget.onLightningSwitchPress,
)
: ChainIcon(
iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
Expanded(
child: SizedBox(
height: 36,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
reverseDuration: Duration.zero,
layoutBuilder: (currentChild, previousChildren) => Stack(
alignment: Alignment.centerLeft,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
),
child: replacesWalletName
? SyncBar(
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
)
: Row(
children: [
Expanded(
child: WalletInfoBar(
name: widget.dashboardViewModel.wallet.name,
hardwareWalletType:
widget.dashboardViewModel.wallet.hardwareWalletType,
),
),
if (widget.dashboardViewModel.isTorEnabled) ...[
const SizedBox(width: 6),
SyncBar(
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
],
],
),
),
),
),
ModernButton.svg(
iconColor: Theme.of(context).colorScheme.primary,
size: 36,
onPressed: () {
HapticFeedback.mediumImpact();
widget.onSettingsButtonPress();
},
svgPath: "assets/new-ui/top-settings.svg",
semanticLabel: S.of(context).settings_title,
),
],
),
),
),
);
);
//FIXME remove after this gets fixed flutter-side
double _additionalTopPadding(BuildContext context) {
if (Platform.isIOS && MediaQuery.of(context).viewPadding.top < 12) return 24;
if (Platform.isIOS && MediaQuery.of(context).viewPadding.top < 12) {
return 24;
}
return 0;
}
+42 -77
View File
@@ -1,102 +1,67 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
class WalletInfoBar extends StatelessWidget {
const WalletInfoBar(
{super.key,
required this.lightningMode,
required this.name,
required this.hardwareWalletType,
required this.onCustomizeButtonTap,
required this.hasCustomize});
const WalletInfoBar({
required this.name,
required this.hardwareWalletType,
super.key,
});
final bool lightningMode;
final String name;
final HardwareWalletType? hardwareWalletType;
final bool hasCustomize;
final VoidCallback onCustomizeButtonTap;
void _openAccountCustomizer() {
if (hasCustomize) {
onCustomizeButtonTap();
HapticFeedback.mediumImpact();
}
}
@override
Widget build(BuildContext context) {
// 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,
children: [
AnimatedSwitcher(
duration: Duration(milliseconds: 150),
transitionBuilder: (child, animation) {
return SizeTransition(
return Semantics(
label: semanticsLabel,
child: ExcludeSemantics(
child: Row(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
transitionBuilder: (child, animation) => SizeTransition(
axis: Axis.horizontal,
sizeFactor: animation,
child: FadeTransition(opacity: animation, child: child),
);
},
child: hardwareWalletIcon == null
? const SizedBox.shrink(key: ValueKey("empty"))
: Padding(
padding: const EdgeInsets.only(right: 8),
child: CakeImageWidget(
imageUrl: hardwareWalletIcon!,
key: ValueKey("hardware_wallet_icon"),
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
child: hardwareWalletIcon == null
? const SizedBox.shrink(key: ValueKey("empty"))
: Padding(
padding: const EdgeInsets.only(right: 4),
child: CakeImageWidget(
imageUrl: hardwareWalletIcon!,
key: const ValueKey("hardware_wallet_icon"),
width: 24,
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
),
),
),
Text(
name,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(color: Theme.of(context).colorScheme.onSurface),
),
if (hasCustomize) ...[
SizedBox(width: 8),
ModernButton.svg(
size: 24,
onPressed: _openAccountCustomizer,
svgPath: "assets/new-ui/icon-accounts.svg",
semanticLabel: S.of(context).wallet_accounts,
)
]
],
),
Expanded(
child: Text(
name,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(color: Theme.of(context).colorScheme.onSurface),
),
),
],
),
),
);
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 {
+8
View File
@@ -4,6 +4,7 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import "package:cake_wallet/core/auth_service.dart";
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/new-ui/new_dashboard.dart';
import "package:cake_wallet/new-ui/pages/account_customizer.dart";
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
@@ -670,6 +671,13 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) => getIt.get<MoneroAccountEditOrCreatePage>(
param1: settings.arguments as AccountListItem?));
case Routes.accountCustomizer:
return handleRouteWithPlatformAwareness(
(_) => getIt.get<AccountCustomizer>(
param1: settings.arguments! as DashboardViewModel,
),
);
case Routes.nanoAccountCreation:
return CupertinoPageRoute<String>(
builder: (_) =>
+1
View File
@@ -32,6 +32,7 @@ class Routes {
static const login = '/login';
static const splash = '/splash';
static const accountCreation = '/account_new';
static const accountCustomizer = "/account_customizer";
static const nanoAccountCreation = '/nano_account_new';
static const addressBook = '/address_book';
static const pickerAddressBook = '/picker_address_book';