| 1 | import 'package:cake_wallet/core/sync_status_title.dart'; |
| 2 | import 'package:cake_wallet/di.dart'; |
| 3 | import 'package:cake_wallet/generated/i18n.dart'; |
| 4 | import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; |
| 5 | import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; |
| 6 | import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; |
| 7 | import 'package:cw_core/sync_status.dart'; |
| 8 | import "package:flutter/cupertino.dart"; |
| 9 | import 'package:flutter/material.dart'; |
| 10 | import 'package:flutter_mobx/flutter_mobx.dart'; |
| 11 | import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; |
| 12 | |
| 13 | class SyncBar extends StatelessWidget { |
| 14 | SyncBar({ |
| 15 | super.key, |
| 16 | required this.dashboardViewModel, |
| 17 | required this.isSyncHeavy, |
| 18 | required this.showSyncedMessage, |
| 19 | }); |
| 20 | |
| 21 | final DashboardViewModel dashboardViewModel; |
| 22 | final bool isSyncHeavy; |
| 23 | final bool showSyncedMessage; |
| 24 | |
| 25 | static const failStatuses = [ |
| 26 | FailedSyncStatus, |
| 27 | LostConnectionSyncStatus, |
| 28 | TimedOutSyncStatus, |
| 29 | UnsupportedSyncStatus, |
| 30 | ]; |
| 31 | |
| 32 | static const progressStatuses = [ |
| 33 | SyncingSyncStatus, |
| 34 | NotConnectedSyncStatus, |
| 35 | SyncronizingSyncStatus, |
| 36 | AttemptingSyncStatus, |
| 37 | StartingScanSyncStatus, |
| 38 | AttemptingScanSyncStatus, |
| 39 | SyncedTipSyncStatus, |
| 40 | ProcessingSyncStatus, |
| 41 | ConnectingSyncStatus, |
| 42 | ConnectedSyncStatus, |
| 43 | ]; |
| 44 | |
| 45 | static const syncedColor = Color(0xFF12A439); |
| 46 | |
| 47 | @override |
| 48 | Widget build(BuildContext context) => Observer( |
| 49 | builder: (_) { |
| 50 | final status = dashboardViewModel.status; |
| 51 | final Widget? icon = _getIcon(context, status.runtimeType); |
| 52 | |
| 53 | return Expanded( |
| 54 | child: Stack( |
| 55 | alignment: Alignment.centerLeft, |
| 56 | children: [ |
| 57 | if (!_showFullBar()) _buildCompactBar(context), |
| 58 | if (_showFullBar()) |
| 59 | // A single node: the localized status text (plus any active |
| 60 | // Tor/MWEB/Silent Payments badge) is the label, and the hint says |
| 61 | // where tapping leads. Everything inside is redundant with it. |
| 62 | Semantics( |
| 63 | button: true, |
| 64 | label: _statusSemanticsLabel(context, status), |
| 65 | hint: S.of(context).manage_nodes, |
| 66 | onTap: () => _openNodeManagement(context), |
| 67 | child: ExcludeSemantics( |
| 68 | child: GestureDetector( |
| 69 | onTap: () => _openNodeManagement(context), |
| 70 | child: AnimatedSwitcher( |
| 71 | duration: Duration(milliseconds: 100), |
| 72 | child: Container( |
| 73 | key: ValueKey(status.runtimeType), |
| 74 | height: 36, |
| 75 | decoration: BoxDecoration( |
| 76 | borderRadius: BorderRadius.circular(9999), |
| 77 | border: _getBorder(context, status.runtimeType), |
| 78 | color: _getBackgroundColor(context, status.runtimeType), |
| 79 | ), |
| 80 | child: Row( |
| 81 | spacing: 10, |
| 82 | mainAxisAlignment: MainAxisAlignment.center, |
| 83 | crossAxisAlignment: CrossAxisAlignment.center, |
| 84 | mainAxisSize: MainAxisSize.max, |
| 85 | children: [ |
| 86 | if (icon != null) icon, |
| 87 | // if (dashboardViewModel.silentPaymentsScanningActive && |
| 88 | // progressStatuses.contains(status.runtimeType)) ...[ |
| 89 | // Text( |
| 90 | // "${(status.progress() * 100).toInt()}%", |
| 91 | // style: TextStyle(fontSize: 12, color: Color(0xFFEFBA5E)), |
| 92 | // ), |
| 93 | // Text( |
| 94 | // "·", |
| 95 | // style: TextStyle(fontSize: 12), |
| 96 | // ) |
| 97 | // ], |
| 98 | Text( |
| 99 | syncStatusTitle(status, |
| 100 | dashboardViewModel.settingsStore.syncStatusDisplayMode), |
| 101 | style: _getTextStyle(context, status.runtimeType), |
| 102 | ), |
| 103 | ], |
| 104 | ), |
| 105 | ), |
| 106 | ), |
| 107 | ), |
| 108 | ), |
| 109 | ), |
| 110 | ], |
| 111 | ), |
| 112 | ); |
| 113 | }, |
| 114 | ); |
| 115 | |
| 116 | void _openNodeManagement(BuildContext context) => |
| 117 | CupertinoScaffold.showCupertinoModalBottomSheet( |
| 118 | context: context, |
| 119 | barrierColor: Colors.black.withAlpha(85), |
| 120 | builder: (context) => FractionallySizedBox( |
| 121 | child: Material( |
| 122 | child: getIt.get<ManageNodesPage>(param1: false), |
| 123 | ))); |
| 124 | |
| 125 | /// Compact mode shows sync state with a pulsing dot (and a Tor glyph) only, so |
| 126 | /// the whole row needs a text equivalent. |
| 127 | Widget _buildCompactBar(BuildContext context) { |
| 128 | final row = Row( |
| 129 | mainAxisSize: MainAxisSize.min, |
| 130 | spacing: 6, |
| 131 | children: [ |
| 132 | if (dashboardViewModel.isTorEnabled) |
| 133 | CakeImageWidget( |
| 134 | imageUrl: "assets/new-ui/tor.svg", |
| 135 | width: 20, |
| 136 | height: 20, |
| 137 | ), |
| 138 | if (_showDot()) const CupertinoActivityIndicator(radius: 8), |
| 139 | if(_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18), |
| 140 | ], |
| 141 | ); |
| 142 | |
| 143 | final label = _joinLabels([ |
| 144 | if (dashboardViewModel.isTorEnabled) S.of(context).tor_connection, |
| 145 | if (_showDot()) S.of(context).synchronizing, |
| 146 | ]); |
| 147 | |
| 148 | if (label.isEmpty) return row; |
| 149 | |
| 150 | return Semantics(label: label, child: ExcludeSemantics(child: row)); |
| 151 | } |
| 152 | |
| 153 | bool get _isShowingSyncedMessage => |
| 154 | showSyncedMessage && dashboardViewModel.status.runtimeType == SyncedSyncStatus; |
| 155 | |
| 156 | String _statusSemanticsLabel(BuildContext context, SyncStatus status) { |
| 157 | final isFailure = failStatuses.contains(status.runtimeType); |
| 158 | |
| 159 | return _joinLabels([ |
| 160 | syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode), |
| 161 | if (!isFailure && dashboardViewModel.isTorEnabled) S.of(context).tor_connection, |
| 162 | if (!isFailure && dashboardViewModel.hasMweb) S.of(context).litecoin_mweb, |
| 163 | if (!isFailure && dashboardViewModel.hasSilentPayments) S.of(context).silent_payments, |
| 164 | ]); |
| 165 | } |
| 166 | |
| 167 | String _joinLabels(List<String> parts) => parts.where((part) => part.isNotEmpty).join(", "); |
| 168 | |
| 169 | Color? _getBackgroundColor(BuildContext context, Type status) { |
| 170 | if (failStatuses.contains(status)) { |
| 171 | return Theme.of(context).colorScheme.errorContainer.withAlpha(64); |
| 172 | } |
| 173 | |
| 174 | return null; |
| 175 | } |
| 176 | |
| 177 | Border? _getBorder(BuildContext context, Type status) { |
| 178 | if (progressStatuses.contains(status) || _isShowingSyncedMessage) { |
| 179 | return Border.all(color: Theme.of(context).colorScheme.surfaceContainerHigh, width: 1); |
| 180 | } |
| 181 | |
| 182 | return null; |
| 183 | } |
| 184 | |
| 185 | TextStyle? _getTextStyle(BuildContext context, Type status) { |
| 186 | final Color color; |
| 187 | if (failStatuses.contains(status)) { |
| 188 | color = Theme.of(context).colorScheme.error; |
| 189 | } else if (status == SyncedSyncStatus) { |
| 190 | color = syncedColor; |
| 191 | } else { |
| 192 | color = Theme.of(context).colorScheme.onSurfaceVariant; |
| 193 | } |
| 194 | |
| 195 | return TextStyle( |
| 196 | fontSize: 12, fontWeight: FontWeight.w400, color: color); |
| 197 | } |
| 198 | |
| 199 | Widget? _getIcon(BuildContext context, Type status) { |
| 200 | |
| 201 | if(status == SyncedSyncStatus) { |
| 202 | return Icon(Icons.check, color: syncedColor, size: 12); |
| 203 | } |
| 204 | |
| 205 | if (status == LostConnectionSyncStatus) { |
| 206 | return CakeImageWidget( |
| 207 | imageUrl: "assets/new-ui/offline.svg", |
| 208 | colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn), |
| 209 | ); |
| 210 | } |
| 211 | |
| 212 | if (failStatuses.contains(status)) { |
| 213 | return CakeImageWidget( |
| 214 | imageUrl: "assets/new-ui/warning.svg", |
| 215 | colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn), |
| 216 | ); |
| 217 | } |
| 218 | |
| 219 | final List<Widget> children = []; |
| 220 | |
| 221 | if (dashboardViewModel.isTorEnabled) { |
| 222 | children.add(CakeImageWidget( |
| 223 | imageUrl: "assets/new-ui/tor_sync.svg", |
| 224 | colorFilter: ColorFilter.mode(Color(0xFF8A38F5), BlendMode.srcIn))); |
| 225 | } |
| 226 | if (dashboardViewModel.hasMweb) { |
| 227 | children.add(CakeImageWidget( |
| 228 | imageUrl: "assets/new-ui/mweb_sync.svg", |
| 229 | colorFilter: |
| 230 | ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), |
| 231 | )); |
| 232 | } |
| 233 | if (dashboardViewModel.hasSilentPayments) { |
| 234 | children.add(CakeImageWidget( |
| 235 | imageUrl: "assets/new-ui/silent_sync.svg", |
| 236 | colorFilter: ColorFilter.mode(Color(0xFFEFBA5E), BlendMode.srcIn), |
| 237 | )); |
| 238 | } |
| 239 | |
| 240 | return Row( |
| 241 | spacing: 8, |
| 242 | children: children, |
| 243 | ); |
| 244 | } |
| 245 | |
| 246 | bool _showFullBar() { |
| 247 | if (dashboardViewModel.status.runtimeType == SyncedSyncStatus) |
| 248 | return isSyncHeavy && _isShowingSyncedMessage; |
| 249 | return isSyncHeavy || failStatuses.contains(dashboardViewModel.status.runtimeType); |
| 250 | } |
| 251 | |
| 252 | bool _showDot() => |
| 253 | !isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType); |
| 254 | |
| 255 | bool _showLightSyncCheck() => |
| 256 | !isSyncHeavy && _isShowingSyncedMessage; |
| 257 | } |