dev
dart 547 lines 18.5 KB
Raw
1 // ignore_for_file: deprecated_member_use
2
3 import 'dart:math';
4
5 import 'package:cake_wallet/entities/seed_type.dart';
6 import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
7 import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
8 import 'package:cake_wallet/themes/core/theme_extension.dart';
9 import 'package:cake_wallet/utils/responsive_layout_util.dart';
10 import 'package:cw_core/crypto_currency.dart';
11 import 'package:cw_core/transaction_priority.dart';
12 import 'package:flutter/material.dart';
13 import 'package:cw_core/currency.dart';
14 import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart';
15
16 class Picker<Item> extends StatefulWidget {
17 Picker({
18 required this.selectedAtIndex,
19 required this.items,
20 required this.onItemSelected,
21 this.title,
22 this.displayItem,
23 this.displayItemColor,
24 this.displayItemFontSize,
25 this.images = const <Widget>[],
26 this.description,
27 this.mainAxisAlignment = MainAxisAlignment.start,
28 this.isGridView = false,
29 this.isSeparated = true,
30 this.hintText,
31 this.headerEnabled = true,
32 this.closeOnItemSelected = true,
33 this.sliderValue,
34 this.minValue,
35 this.maxValue,
36 this.customItemIndex,
37 this.isWrapped = true,
38 this.borderColor,
39 this.onSliderChanged,
40 this.matchingCriteria,
41 this.hasTitleSpacing = false,
42 }) : assert(hintText == null || matchingCriteria != null) {
43 // make sure that if the search field is enabled then there is a searching criteria provided
44 if (sliderValue != null && maxValue != null) {
45 if (sliderValue! > maxValue!) {
46 sliderValue = maxValue;
47 }
48 }
49 }
50
51 final int selectedAtIndex;
52 final List<Item> items;
53 final List<Widget> images;
54 final String? title;
55 final String? description;
56 final Function(Item) onItemSelected;
57 final MainAxisAlignment mainAxisAlignment;
58 final String Function(Item)? displayItem;
59 final Color? displayItemColor;
60 final double? displayItemFontSize;
61 final bool isGridView;
62 final bool isSeparated;
63 final String? hintText;
64 final bool headerEnabled;
65 final bool closeOnItemSelected;
66 double? sliderValue;
67 final double? minValue;
68 final int? customItemIndex;
69 final bool isWrapped;
70 final Color? borderColor;
71 final Function(double)? onSliderChanged;
72 final bool Function(Item, String)? matchingCriteria;
73 final double? maxValue;
74 final bool hasTitleSpacing;
75 @override
76 _PickerState<Item> createState() => _PickerState<Item>(items, images, onItemSelected);
77 }
78
79 class _PickerState<Item> extends State<Picker<Item>> {
80 _PickerState(this.items, this.images, this.onItemSelected);
81
82 final Function(Item) onItemSelected;
83 List<Item> items;
84 List<Widget> images;
85 List<Item> filteredItems = [];
86 List<Widget> filteredImages = [];
87 final TextEditingController searchController = TextEditingController();
88
89 ScrollController controller = ScrollController();
90
91 void clearFilteredItemsList() {
92 filteredItems = List.from(
93 items,
94 growable: true,
95 );
96 filteredImages = List.from(
97 images,
98 growable: true,
99 );
100
101 if (widget.selectedAtIndex != -1) {
102 if (widget.selectedAtIndex < filteredItems.length) {
103 filteredItems.removeAt(widget.selectedAtIndex);
104 }
105
106 if (widget.selectedAtIndex < filteredImages.length) {
107 filteredImages.removeAt(widget.selectedAtIndex);
108 }
109 }
110 }
111
112 @override
113 void initState() {
114 super.initState();
115
116 clearFilteredItemsList();
117
118 searchController.addListener(() {
119 clearFilteredItemsList();
120
121 setState(() {
122 filteredItems = List.from(items.where((element) {
123 if (widget.selectedAtIndex != items.indexOf(element) &&
124 (widget.matchingCriteria?.call(element, searchController.text) ?? true)) {
125 if (images.isNotEmpty) {
126 filteredImages.add(images[items.indexOf(element)]);
127 }
128 return true;
129 }
130
131 if (filteredImages.isNotEmpty) {
132 filteredImages.remove(images[items.indexOf(element)]);
133 }
134 return false;
135 }), growable: true);
136
137 return;
138 });
139 });
140 }
141
142 @override
143 Widget build(BuildContext context) {
144 final double padding = 24;
145
146 final mq = MediaQuery.of(context);
147 final bottom = mq.viewInsets.bottom;
148 final height = mq.size.height - bottom;
149
150 double containerHeight = height * 0.65;
151 if (bottom > 0) {
152 // increase a bit or it gets too squished in the top
153 containerHeight = height * 0.75;
154 }
155
156 final content = Column(
157 children: [
158 if (widget.title?.isNotEmpty ?? false)
159 Container(
160 padding: EdgeInsets.symmetric(horizontal: padding),
161 child: Text(
162 key: ValueKey('picker_title_text_key'),
163 widget.title!,
164 textAlign: TextAlign.center,
165 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
166 fontSize: 18,
167 fontWeight: FontWeight.bold,
168 decoration: TextDecoration.none,
169 color: Theme.of(context).colorScheme.onSurface,
170 ),
171 ),
172 ),
173 widget.hasTitleSpacing ? const SizedBox(height: 24) : const SizedBox.shrink(),
174 Padding(
175 padding: EdgeInsets.symmetric(horizontal: padding),
176 child: Container(
177 decoration: BoxDecoration(
178 borderRadius: BorderRadius.circular(30),
179 border: Border.all(
180 color: widget.borderColor ?? Colors.transparent,
181 ),
182 ),
183 child: ClipRRect(
184 borderRadius: BorderRadius.all(Radius.circular(30)),
185 child: Container(
186 color: Theme.of(context).colorScheme.surface,
187 child: ConstrainedBox(
188 constraints: BoxConstraints(
189 maxHeight: containerHeight,
190 maxWidth: ResponsiveLayoutUtilBase.kPopupWidth,
191 ),
192 child: Column(
193 mainAxisSize: MainAxisSize.min,
194 children: [
195 if (widget.hintText != null) ...[
196 Padding(
197 padding: const EdgeInsets.all(16),
198 child: SearchBarWidget(
199 key: ValueKey('picker_search_bar_key'),
200 searchController: searchController,
201 hintText: widget.hintText,
202 ),
203 ),
204 Divider(
205 color: Theme.of(context).colorScheme.outlineVariant,
206 height: 1,
207 ),
208 ],
209 if (widget.selectedAtIndex != -1 && widget.headerEnabled)
210 buildSelectedItem(widget.selectedAtIndex),
211 Flexible(
212 child: Stack(
213 alignment: Alignment.center,
214 children: <Widget>[
215 filteredItems.length > 3
216 ? Scrollbar(
217 key: ValueKey('picker_scrollbar_key'),
218 controller: controller,
219 child: itemsList(),
220 )
221 : itemsList(),
222 (widget.description?.isNotEmpty ?? false)
223 ? Positioned(
224 bottom: padding,
225 left: padding,
226 right: padding,
227 child: Text(
228 key: ValueKey('picker_descriptinon_text_key'),
229 widget.description!,
230 textAlign: TextAlign.center,
231 style: Theme.of(context).textTheme.bodySmall!.copyWith(
232 fontWeight: FontWeight.w500,
233 decoration: TextDecoration.none,
234 ),
235 ),
236 )
237 : Offstage(),
238 ],
239 ),
240 ),
241 ],
242 ),
243 ),
244 ),
245 ),
246 ),
247 )
248 ],
249 );
250
251 if (widget.isWrapped) {
252 return PickerWrapperWidget(
253 key: ValueKey('picker_wrapper_widget_key'),
254 hasTitle: widget.title?.isNotEmpty ?? false,
255 children: [content],
256 );
257 } else {
258 return content;
259 }
260 }
261
262 Widget itemsList() {
263 final itemCount = !widget.headerEnabled
264 ? items.length
265 : filteredItems.isEmpty
266 ? 0
267 : filteredItems.length;
268 return Container(
269 color: Theme.of(context).colorScheme.outlineVariant,
270 child: widget.isGridView
271 ? GridView.builder(
272 key: ValueKey('picker_items_grid_view_key'),
273 padding: EdgeInsets.zero,
274 controller: controller,
275 shrinkWrap: true,
276 itemCount: itemCount,
277 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
278 crossAxisCount: 2,
279 crossAxisSpacing: 2,
280 childAspectRatio: 3,
281 ),
282 itemBuilder: (context, index) =>
283 !widget.headerEnabled && widget.selectedAtIndex == index
284 ? buildSelectedItem(index)
285 : buildItem(index),
286 )
287 : ListView.separated(
288 key: ValueKey('picker_items_list_view_key'),
289 padding: EdgeInsets.zero,
290 controller: controller,
291 shrinkWrap: true,
292 separatorBuilder: (context, index) => widget.isSeparated
293 ? Divider(
294 color: Theme.of(context).colorScheme.outlineVariant,
295 height: 1,
296 )
297 : const SizedBox(),
298 itemCount: itemCount,
299 itemBuilder: (context, index) =>
300 !widget.headerEnabled && widget.selectedAtIndex == index
301 ? buildSelectedItem(index)
302 : buildItem(index),
303 ),
304 );
305 }
306
307 String _getItemName(Item item) {
308 String itemName;
309 if (item is Currency) {
310 itemName = item.name;
311 } else if (item is TransactionPriority) {
312 itemName = item.title;
313 } else if (item is MoneroSeedType) {
314 itemName = item.title;
315 } else {
316 itemName = '';
317 }
318
319 return itemName;
320 }
321
322 Widget buildItem(int index) {
323 final item = widget.headerEnabled ? filteredItems[index] : items[index];
324
325 final tag = item is Currency ? item.tag : null;
326 final itemName = _getItemName(item);
327
328 final icon = _getItemIcon(item);
329
330 final image = images.isNotEmpty ? filteredImages[index] : icon;
331
332 final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex;
333
334 final itemContent = Row(
335 mainAxisSize: MainAxisSize.max,
336 mainAxisAlignment: widget.mainAxisAlignment,
337 crossAxisAlignment: CrossAxisAlignment.center,
338 children: <Widget>[
339 image ?? Offstage(),
340 Expanded(
341 child: Padding(
342 padding: EdgeInsets.only(left: image != null ? 12 : 0),
343 child: Row(
344 children: [
345 Flexible(
346 child: Text(
347 key: ValueKey('picker_items_index_${itemName}_text_key'),
348 widget.displayItem?.call(item) ??
349 (item == CryptoCurrency.btcln ? "BTC (LN)" : item.toString()),
350 softWrap: true,
351 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
352 fontWeight: FontWeight.w600,
353 decoration: TextDecoration.none,
354 color: widget.displayItemColor,
355 ),
356 ),
357 ),
358 if (tag != null && item != CryptoCurrency.btcln)
359 Align(
360 alignment: Alignment.topCenter,
361 child: Container(
362 width: 35.0,
363 height: 18.0,
364 child: Center(
365 child: Text(
366 key: ValueKey('picker_items_index_${index}_tag_key'),
367 tag,
368 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
369 fontSize: 7.0,
370 color: Theme.of(context).colorScheme.primary,
371 ),
372 ),
373 ),
374 decoration: BoxDecoration(
375 borderRadius: BorderRadius.circular(6.0),
376 //border: Border.all(color: ),
377 color: Theme.of(context).colorScheme.surfaceVariant,
378 ),
379 ),
380 ),
381 ],
382 ),
383 ),
384 ),
385 ],
386 );
387
388 return GestureDetector(
389 key: ValueKey('picker_items_index_${itemName}_button_key'),
390 onTap: () {
391 if (widget.closeOnItemSelected) Navigator.of(context).pop();
392 onItemSelected(item!);
393 },
394 child: Container(
395 height: isCustomItem ? 95 : 55,
396 color: Theme.of(context).colorScheme.surface,
397 padding: EdgeInsets.symmetric(horizontal: 24),
398 child: isCustomItem
399 ? Column(
400 mainAxisAlignment: MainAxisAlignment.spaceAround,
401 children: [
402 itemContent,
403 buildSlider(index: index, isActivated: widget.selectedAtIndex == index)
404 ],
405 )
406 : itemContent,
407 ),
408 );
409 }
410
411 Widget buildSelectedItem(int index) {
412 final item = items[index];
413
414 final tag = item is Currency ? item.tag : null;
415 final itemName = _getItemName(item);
416 final icon = _getItemIcon(item);
417
418 final image = images.isNotEmpty ? images[index] : icon;
419
420 final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex;
421
422 final itemContent = Row(
423 key: ValueKey('picker_selected_item_row_key'),
424 mainAxisSize: MainAxisSize.max,
425 mainAxisAlignment: widget.mainAxisAlignment,
426 crossAxisAlignment: CrossAxisAlignment.center,
427 children: <Widget>[
428 image ?? Offstage(),
429 Expanded(
430 child: Padding(
431 padding: EdgeInsets.only(left: image != null ? 12 : 0),
432 child: Row(
433 children: [
434 Flexible(
435 child: Text(
436 key: ValueKey('picker_items_index_${itemName}_selected_item_text_key'),
437 widget.displayItem?.call(item) ?? item.toString(),
438 softWrap: true,
439 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
440 fontSize: 16,
441 fontWeight: FontWeight.w700,
442 color: Theme.of(context).colorScheme.onSurface,
443 decoration: TextDecoration.none,
444 ),
445 ),
446 ),
447 if (tag != null)
448 Align(
449 alignment: Alignment.topCenter,
450 child: Container(
451 width: 35.0,
452 height: 18.0,
453 child: Center(
454 child: Text(
455 tag,
456 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
457 fontSize: 7.0,
458 color: Theme.of(context).colorScheme.primary,
459 ),
460 ),
461 ),
462 decoration: BoxDecoration(
463 borderRadius: BorderRadius.circular(6.0),
464 //border: Border.all(color: ),
465 color: Theme.of(context).colorScheme.surfaceVariant,
466 ),
467 ),
468 ),
469 ],
470 ),
471 ),
472 ),
473 Icon(Icons.check_circle, color: Theme.of(context).colorScheme.primary),
474 ],
475 );
476
477 return GestureDetector(
478 key: ValueKey('picker_items_index_${itemName}_selected_item_button_key'),
479 onTap: () {
480 if (widget.closeOnItemSelected) Navigator.of(context).pop();
481 },
482 child: Container(
483 height: isCustomItem ? 95 : 55,
484 color: Theme.of(context).colorScheme.surface,
485 padding: EdgeInsets.symmetric(horizontal: 24),
486 child: isCustomItem
487 ? Column(
488 mainAxisAlignment: MainAxisAlignment.spaceAround,
489 children: [
490 itemContent,
491 buildSlider(index: index, isActivated: widget.selectedAtIndex == index)
492 ],
493 )
494 : itemContent,
495 ),
496 );
497 }
498
499 Widget? _getItemIcon(Item item) {
500 if (item is Currency) {
501 if (item.iconPath != null) {
502 return TokenImageWidget(
503 imageUrl: item.iconPath!,
504 size: 20.0,
505 );
506 } else {
507 return Container(
508 height: 20.0,
509 width: 20.0,
510 child: Center(
511 child: Text(
512 item.name.substring(0, min(item.name.length, 2)).toUpperCase(),
513 style: Theme.of(context).textTheme.bodySmall?.copyWith(
514 fontSize: 11,
515 ),
516 ),
517 ),
518 decoration: BoxDecoration(
519 shape: BoxShape.circle,
520 color: Theme.of(context).colorScheme.surfaceContainer,
521 ),
522 );
523 }
524 }
525
526 return null;
527 }
528
529 Widget buildSlider({required int index, required bool isActivated}) {
530 return Row(
531 children: <Widget>[
532 Expanded(
533 child: Slider(
534 activeColor: Theme.of(context).colorScheme.primary,
535 inactiveColor: context.customColors.toggleColorOffState,
536 thumbColor: context.customColors.toggleKnobStateColor,
537 value: widget.sliderValue == null || widget.sliderValue! < 1 ? 1 : widget.sliderValue!,
538 onChanged: isActivated ? widget.onSliderChanged : null,
539 min: widget.minValue ?? 1,
540 max: (widget.maxValue == null || widget.maxValue! < 1) ? 100 : widget.maxValue!,
541 divisions: 100,
542 ),
543 ),
544 ],
545 );
546 }
547 }