dev
dart 257 lines 9.1 KB
Raw
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 }