dev
dart 538 lines 20.9 KB
Raw
1 import 'dart:ui';
2
3 import 'package:cake_wallet/di.dart';
4 import 'package:cake_wallet/generated/i18n.dart';
5 import 'package:cake_wallet/monero/monero.dart';
6 import 'package:cake_wallet/new-ui/long_press_popup.dart';
7 import 'package:cake_wallet/new-ui/widgets/addresses_page/address_info.dart';
8 import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
9 import 'package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart';
10 import 'package:cake_wallet/new-ui/widgets/long_press_menu.dart';
11 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
12 import 'package:cake_wallet/routes.dart';
13 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
14 import 'package:cake_wallet/utils/address_formatter.dart';
15 import 'package:cake_wallet/utils/show_pop_up.dart';
16 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
17 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
18 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
19 import 'package:cake_wallet/wownero/wownero.dart';
20 import 'package:cw_core/card_design.dart';
21 import 'package:cw_core/wallet_type.dart';
22 import 'package:flutter/material.dart';
23 import 'package:flutter/semantics.dart';
24 import 'package:flutter_mobx/flutter_mobx.dart';
25
26 import 'package:cake_wallet/utils/list_item.dart';
27 import 'package:mobx/mobx.dart';
28 import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
29
30 class NewAddressesPage extends StatefulWidget {
31 const NewAddressesPage(
32 {super.key,
33 required this.addressListViewModel,
34 required this.dashboardViewModel,
35 required this.showHidden});
36
37 final WalletAddressListViewModel addressListViewModel;
38 final DashboardViewModel dashboardViewModel;
39 final bool showHidden;
40
41 @override
42 State<NewAddressesPage> createState() => _NewAddressesPageState();
43 }
44
45 class _NewAddressesPageState extends State<NewAddressesPage> {
46 List<ListItem> getItems(List<ListItem> list) {
47 return list.where((element) {
48 if (element is WalletAddressListItem) {
49 if (!element.isHidden && !widget.showHidden) return true;
50 if (element.isHidden && widget.showHidden) return true;
51 return false;
52 }
53 return false;
54 }).toList();
55 }
56
57 List<ListItem> items = [];
58
59 late final TextEditingController _searchController;
60
61 CardDesign? design;
62
63 void updateItems() {
64 setState(() {
65 items = getItems(widget.addressListViewModel.forceRecomputeItems);
66 });
67 }
68
69 @override
70 void initState() {
71 super.initState();
72
73 items = getItems(widget.addressListViewModel.items);
74 _searchController = TextEditingController()
75 ..addListener(() {
76 setState(() {});
77 });
78
79 WidgetsBinding.instance.addPostFrameCallback((_) {
80 reaction((context) => widget.dashboardViewModel.cardDesigns.first, (value) {
81 setState(() {
82 design = value;
83 });
84 });
85 });
86 }
87
88 @override
89 Widget build(BuildContext context) {
90 final filteredItems = items.where((item) {
91 if (item is! WalletAddressListItem) return false;
92 if (_searchController.text.isEmpty) return true;
93 if (item.name == null || item.name!.isEmpty) {
94 return (item.address.contains(_searchController.text.toLowerCase()));
95 }
96 return (item.name ?? '').toLowerCase().contains(_searchController.text.toLowerCase());
97 }).toList();
98
99 return Container(
100 decoration: BoxDecoration(
101 color: Theme.of(context).colorScheme.surface,
102 borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
103 child: Column(
104 spacing: 12.0,
105 children: [
106 ModalTopBar(
107 title: widget.showHidden ? S.of(context).hidden_addresses : S.of(context).addresses,
108 leadingIcon: Icon(Icons.arrow_back),
109 leadingSemanticLabel: S.of(context).seed_alert_back,
110 onLeadingPressed: Navigator.of(context).pop,
111 onTrailingPressed: () {}),
112 Expanded(
113 child: Stack(
114 children: [
115 CustomScrollView(
116 controller: ModalScrollController.of(context),
117 slivers: [
118 if (!widget.showHidden)
119 SliverToBoxAdapter(
120 child: Column(
121 spacing: 16,
122 children: [
123 if (widget.dashboardViewModel.type == WalletType.monero ||
124 widget.dashboardViewModel.type == WalletType.wownero)
125 Observer(
126 builder: (_) => AccountPreviewHeader(
127 dashboardViewModel: widget.dashboardViewModel,
128 design: design,
129 )),
130 Text(
131 S.of(context).long_press_edit_address,
132 style: TextStyle(
133 fontSize: 10,
134 color: Theme.of(context).colorScheme.onSurfaceVariant),
135 ),
136 ShowHiddenButton()
137 ],
138 ),
139 ),
140 SliverPadding(
141 padding: EdgeInsets.only(bottom: 64),
142 sliver: SliverList.separated(
143 itemCount: filteredItems.length,
144 itemBuilder: (context, index) {
145 final item = filteredItems[index];
146 if (item is WalletAddressListItem) {
147 return Observer(
148 builder: (_) => AddressRow(
149 selected:
150 item.address == widget.addressListViewModel.address.address,
151 first: widget.showHidden && index == 0,
152 last: index == filteredItems.length - 1,
153 item: item,
154 onSelect: () {
155 widget.addressListViewModel.setAddress(item);
156 },
157 onLabelChanged: updateItems,
158 onAddressHidden: () async {
159 await widget.addressListViewModel.toggleHideAddress(item);
160 updateItems();
161 },
162 walletType: widget.addressListViewModel.type,
163 hasBalance: widget.addressListViewModel.isBalanceAvailable,
164 hasReceived: widget.addressListViewModel.isReceivedAvailable,
165 ),
166 );
167 }
168 return SizedBox.shrink();
169 },
170 separatorBuilder: (_, __) => Padding(
171 padding: EdgeInsets.symmetric(horizontal: 16),
172 child: Container(
173 height: 1,
174 color: Theme.of(context).colorScheme.surfaceContainerHigh,
175 ),
176 ),
177 ),
178 ),
179 SliverToBoxAdapter(child: SizedBox(height: 72))
180 ],
181 ),
182 SafeArea(
183 child: Padding(
184 padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
185 child: AddressSearchBox(controller: _searchController)),
186 ),
187 ],
188 ),
189 )
190 ],
191 ),
192 );
193 }
194 }
195
196 class AddressSearchBox extends StatelessWidget {
197 const AddressSearchBox({super.key, required this.controller});
198
199 final TextEditingController controller;
200
201 @override
202 Widget build(BuildContext context) {
203 return Align(
204 alignment: Alignment.bottomCenter,
205 child: Padding(
206 padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 18.0),
207 child: ClipRRect(
208 borderRadius: BorderRadius.circular(999999),
209 child: BackdropFilter(
210 filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
211 child: Container(
212 decoration: BoxDecoration(
213 color: Theme.of(context).colorScheme.surfaceContainerHigh.withAlpha(128),
214 border: Border.all(
215 color: Theme.of(context).colorScheme.surfaceContainerHighest, width: 1),
216 borderRadius: BorderRadius.circular(99999)),
217 // The hint names the field only while it is empty, so the label is
218 // supplied once there is text to keep exactly one announcement.
219 child: MergeSemantics(
220 child: Semantics(
221 label: controller.text.isEmpty ? null : S.of(context).search,
222 child: TextField(
223 controller: controller,
224 decoration: InputDecoration(
225 hintText: S.of(context).search,
226 hintStyle: TextStyle(
227 fontWeight: FontWeight.w600,
228 ),
229 prefixIcon: ExcludeSemantics(child: Icon(Icons.search)),
230 filled: false,
231 enabledBorder: OutlineInputBorder(
232 borderRadius: BorderRadius.circular(99999),
233 borderSide: BorderSide.none,
234 ),
235 focusedBorder: OutlineInputBorder(
236 borderRadius: BorderRadius.circular(99999),
237 borderSide: BorderSide.none,
238 ),
239 contentPadding: EdgeInsets.symmetric(vertical: 8),
240 ),
241 ),
242 ),
243 ),
244 ),
245 ),
246 ),
247 ),
248 );
249 }
250 }
251
252 class AccountPreviewHeader extends StatelessWidget {
253 AccountPreviewHeader({super.key, required this.dashboardViewModel, required this.design});
254
255 final DashboardViewModel dashboardViewModel;
256 final CardDesign? design;
257
258 @override
259 Widget build(BuildContext context) {
260 return Container(
261 height: 64,
262 width: MediaQuery.of(context).size.width * 0.9,
263 decoration: BoxDecoration(
264 color: Theme.of(context).colorScheme.surfaceContainer,
265 borderRadius: BorderRadius.circular(16)),
266 child: Padding(
267 padding: EdgeInsets.all(12.0),
268 child: Row(
269 mainAxisAlignment: MainAxisAlignment.spaceBetween,
270 children: [
271 Row(
272 spacing: 10,
273 children: [
274 Observer(
275 builder: (_) => BalanceCard(
276 borderRadius: 5, width: 50, design: design ?? CardDesign.genericDefault),
277 ),
278 Column(
279 crossAxisAlignment: CrossAxisAlignment.start,
280 mainAxisAlignment: MainAxisAlignment.center,
281 children: [
282 Text(
283 monero?.getCurrentAccount(dashboardViewModel.wallet).label ??
284 wownero?.getCurrentAccount(dashboardViewModel.wallet).label ??
285 "",
286 style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.primary),
287 ),
288 Text(
289 dashboardViewModel.wallet.name,
290 style: TextStyle(
291 fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
292 )
293 ],
294 ),
295 ],
296 ),
297 Row(
298 spacing: 12,
299 children: [
300 Container(
301 width: 1,
302 height: 40,
303 decoration:
304 BoxDecoration(color: Theme.of(context).colorScheme.surfaceContainerHigh),
305 ),
306 Text(dashboardViewModel.balanceViewModel.balances.values.first.availableBalance)
307 ],
308 )
309 ],
310 ),
311 ),
312 );
313 }
314 }
315
316 class AddressRow extends StatelessWidget {
317 const AddressRow({
318 super.key,
319 required this.selected,
320 required this.first,
321 required this.last,
322 required this.item,
323 required this.onSelect,
324 required this.walletType,
325 required this.onLabelChanged,
326 required this.onAddressHidden,
327 required this.hasBalance,
328 required this.hasReceived,
329 });
330
331 final bool selected;
332 final bool first;
333 final bool last;
334 final WalletAddressListItem item;
335 final VoidCallback onSelect;
336 final VoidCallback onLabelChanged;
337 final VoidCallback onAddressHidden;
338 final WalletType walletType;
339 final bool hasBalance;
340 final bool hasReceived;
341
342 @override
343 Widget build(BuildContext context) {
344 final hasLabel = item.name != null && item.name!.isNotEmpty;
345 final hideLabel = item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address;
346
347 Future<void> editLabel() async {
348 final res = await showPopUp(
349 context: context, builder: (context) => getIt.get<AddressLabelInputPopup>(param1: item));
350
351 if (res != null) {
352 onLabelChanged();
353 }
354 }
355
356 Future<void> showInfo() async {
357 await showPopUp(
358 context: context, builder: (context) => getIt.get<AddressInfoPopup>(param1: item));
359 }
360
361 // The row is a single control: selection on tap, and the actions that are
362 // otherwise reachable only by long press exposed as custom actions.
363 return MergeSemantics(
364 child: Semantics(
365 button: true,
366 selected: selected,
367 customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
368 CustomSemanticsAction(label: S.of(context).set_label): editLabel,
369 CustomSemanticsAction(label: hideLabel): onAddressHidden,
370 CustomSemanticsAction(label: S.of(context).info): showInfo,
371 },
372 child: GestureDetector(
373 onTap: onSelect,
374 child: Padding(
375 padding: const EdgeInsets.symmetric(horizontal: 16.0),
376 child: LongPressPopupBuilder(
377 popup: LongPressMenu(
378 items: [
379 LongPressMenuItem(
380 label: S.of(context).set_label,
381 iconPath: "assets/new-ui/address_set_label.svg",
382 onSelected: () {
383 Navigator.of(context, rootNavigator: true).pop();
384 editLabel();
385 }),
386 LongPressMenuItem(
387 label: hideLabel,
388 iconPath: "assets/new-ui/address_hide.svg",
389 onSelected: () {
390 Navigator.of(context, rootNavigator: true).pop();
391 onAddressHidden();
392 },
393 color: Theme.of(context).colorScheme.error),
394 LongPressMenuItem(
395 label: S.of(context).info,
396 iconPath: "assets/images/info_icon.svg",
397 onSelected: () {
398 Navigator.of(context, rootNavigator: true).pop();
399 showInfo();
400 }),
401 ],
402 ),
403 child: AnimatedContainer(
404 duration: Duration(milliseconds: 150),
405 decoration: BoxDecoration(
406 color: selected
407 ? Theme.of(context).colorScheme.surfaceContainerHigh
408 : Theme.of(context).colorScheme.surfaceContainer,
409 borderRadius: BorderRadius.vertical(
410 top: Radius.circular(first ? 16 : 0),
411 bottom: Radius.circular(last ? 16 : 0),
412 ),
413 ),
414 child: Padding(
415 padding: const EdgeInsets.all(12.0),
416 child: Row(
417 spacing: 8,
418 mainAxisSize: MainAxisSize.max,
419 mainAxisAlignment: MainAxisAlignment.spaceBetween,
420 children: [
421 Expanded(
422 child: Column(
423 children: [
424 Row(
425 spacing: 4,
426 children: [
427 if (hasLabel)
428 Text(
429 item.name!,
430 style: TextStyle(
431 fontSize: 12,
432 color: Theme.of(context).colorScheme.onSurface,
433 fontWeight: FontWeight.w500),
434 ),
435 Expanded(
436 child: AddressFormatter.buildSegmentedAddress(
437 address: item.address,
438 walletType: walletType,
439 textAlign: TextAlign.left,
440 evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
441 fontSize: 12,
442 fontWeight: FontWeight.w400,
443 color: hasLabel
444 ? Theme.of(context).colorScheme.onSurfaceVariant
445 : Theme.of(context).colorScheme.onSurface,
446 ),
447 shouldTruncate: (hasLabel)),
448 ),
449 ],
450 ),
451 Row(
452 mainAxisSize: MainAxisSize.max,
453 mainAxisAlignment: MainAxisAlignment.spaceBetween,
454 children: [
455 Text(
456 "${S.of(context).transactions}: ${item.txCount}",
457 style: TextStyle(
458 fontSize: 12,
459 color: Theme.of(context).colorScheme.onSurfaceVariant),
460 ),
461 Text(
462 "${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}",
463 style: TextStyle(
464 fontSize: 12,
465 color: Theme.of(context).colorScheme.onSurfaceVariant)),
466 ],
467 )
468 ],
469 ),
470 ),
471 // The row's selected state carries this information.
472 if (selected)
473 ExcludeSemantics(
474 child: CakeImageWidget(imageUrl: "assets/new-ui/checkmark.svg"))
475 ],
476 ),
477 ),
478 ),
479 ),
480 ),
481 ),
482 ),
483 );
484 }
485 }
486
487 class ShowHiddenButton extends StatelessWidget {
488 const ShowHiddenButton({super.key});
489
490 @override
491 Widget build(BuildContext context) {
492 return Padding(
493 padding: const EdgeInsets.symmetric(horizontal: 16.0),
494 child: Column(
495 children: [
496 Material(
497 child: MergeSemantics(
498 child: Semantics(
499 button: true,
500 child: InkWell(
501 onTap: () {
502 Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true);
503 },
504 child: Container(
505 height: 48,
506 decoration: BoxDecoration(
507 color: Theme.of(context).colorScheme.surfaceContainer,
508 borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
509 ),
510 child: Padding(
511 padding: const EdgeInsets.symmetric(horizontal: 16.0),
512 child: Row(
513 mainAxisAlignment: MainAxisAlignment.spaceBetween,
514 children: [
515 Text(S.of(context).show_hidden_addresses),
516 ExcludeSemantics(
517 child: RotatedBox(
518 quarterTurns: 1,
519 child:
520 CakeImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg")),
521 )
522 ],
523 ),
524 ),
525 ),
526 ),
527 ),
528 ),
529 ),
530 Container(
531 height: 1,
532 color: Theme.of(context).colorScheme.surfaceContainerHigh,
533 ),
534 ],
535 ),
536 );
537 }
538 }