| 1 | import 'package:cake_wallet/generated/i18n.dart'; |
| 2 | import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart'; |
| 3 | import 'package:cake_wallet/new-ui/widgets/currency_picker/chain_chip_strip.dart'; |
| 4 | import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart'; |
| 5 | import 'package:cake_wallet/new-ui/widgets/currency_picker/picker_recents_loader.dart'; |
| 6 | import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart'; |
| 7 | import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_row.dart'; |
| 8 | import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart'; |
| 9 | import 'package:cake_wallet/new-ui/widgets/currency_picker/picker_section_header.dart'; |
| 10 | import 'package:cake_wallet/new-ui/widgets/currency_picker/pill_grid.dart'; |
| 11 | import 'package:cake_wallet/new-ui/widgets/currency_picker/select_network_page.dart'; |
| 12 | import 'package:cake_wallet/wallet_types.g.dart'; |
| 13 | import 'package:cw_core/crypto_currency.dart'; |
| 14 | import 'package:cw_core/currency_for_wallet_type.dart'; |
| 15 | import 'package:cw_core/currency_groups.dart'; |
| 16 | import 'package:cw_core/utils/print_verbose.dart'; |
| 17 | import 'package:cw_core/wallet_type.dart'; |
| 18 | import 'package:flutter/material.dart'; |
| 19 | |
| 20 | class MultiNetworkCurrencyPicker extends StatefulWidget { |
| 21 | const MultiNetworkCurrencyPicker({super.key, required this.args}); |
| 22 | |
| 23 | final CurrencyPickerArgs args; |
| 24 | |
| 25 | @override |
| 26 | State<MultiNetworkCurrencyPicker> createState() => _MultiNetworkCurrencyPickerState(); |
| 27 | } |
| 28 | |
| 29 | class _MultiNetworkCurrencyPickerState extends State<MultiNetworkCurrencyPicker> { |
| 30 | bool _recentsLoaded = false; |
| 31 | WalletType? _selectedNetwork; |
| 32 | List<CryptoCurrency> _recents = const []; |
| 33 | final TextEditingController _searchController = TextEditingController(); |
| 34 | |
| 35 | bool get _isSearching => _searchController.text.trim().isNotEmpty; |
| 36 | |
| 37 | late final List<WalletType> _networks = _computeNetworks(); |
| 38 | |
| 39 | List<WalletType> _computeNetworks() { |
| 40 | final counts = <WalletType, int>{}; |
| 41 | for (final currency in widget.args.items) { |
| 42 | final walletType = cryptoCurrencyOrTokenToWalletType(currency); |
| 43 | if (walletType == null) continue; |
| 44 | counts[walletType] = (counts[walletType] ?? 0) + 1; |
| 45 | } |
| 46 | return counts.entries.where((e) => e.value >= 2).map((e) => e.key).toList(growable: false); |
| 47 | } |
| 48 | |
| 49 | Set<CryptoCurrency> get _natives => |
| 50 | {for (final walletType in availableWalletTypes) walletTypeToCryptoCurrency(walletType)}; |
| 51 | |
| 52 | @override |
| 53 | void initState() { |
| 54 | super.initState(); |
| 55 | _searchController.addListener(() => setState(() {})); |
| 56 | _loadRecents(); |
| 57 | } |
| 58 | |
| 59 | @override |
| 60 | void dispose() { |
| 61 | _searchController.dispose(); |
| 62 | super.dispose(); |
| 63 | } |
| 64 | |
| 65 | Future<void> _loadRecents() async { |
| 66 | try { |
| 67 | final recents = await PickerRecentsLoader.load( |
| 68 | source: widget.args.recentsSource, |
| 69 | visibleItems: widget.args.items, |
| 70 | ); |
| 71 | if (!mounted) return; |
| 72 | setState(() { |
| 73 | _recents = recents; |
| 74 | _recentsLoaded = true; |
| 75 | }); |
| 76 | } catch (e, s) { |
| 77 | printV('load picker recents failed: $e\n$s'); |
| 78 | if (!mounted) return; |
| 79 | setState(() => _recentsLoaded = true); |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | bool _matchesNetwork(CryptoCurrency c) { |
| 84 | if (_selectedNetwork == null) return true; |
| 85 | return cryptoCurrencyOrTokenToWalletType(c) == _selectedNetwork; |
| 86 | } |
| 87 | |
| 88 | List<CryptoCurrency> get _visibleItems { |
| 89 | final query = _searchController.text.trim(); |
| 90 | return widget.args.items |
| 91 | .where(_hasSupportedChain) |
| 92 | .where((c) => currencyMatchesQuery(c, query) && _matchesNetwork(c)) |
| 93 | .toList(growable: false); |
| 94 | } |
| 95 | |
| 96 | bool _hasSupportedChain(CryptoCurrency c) { |
| 97 | if (c.tag == null) return true; |
| 98 | return cryptoCurrencyOrTokenToWalletType(c) != null; |
| 99 | } |
| 100 | |
| 101 | void _selectCurrency(CryptoCurrency currency) { |
| 102 | widget.args.onSelected(currency); |
| 103 | Navigator.of(context).maybePop(); |
| 104 | } |
| 105 | |
| 106 | void _onStablecoinPillTapped(CryptoCurrency tapped) { |
| 107 | var variants = widget.args.items |
| 108 | .where((c) => |
| 109 | isTrustedStablecoin(c) && |
| 110 | c.title.toUpperCase() == tapped.title.toUpperCase() && |
| 111 | cryptoCurrencyOrTokenToWalletType(c) != null) |
| 112 | .toList(); |
| 113 | |
| 114 | if (_selectedNetwork != null) { |
| 115 | final filtered = variants |
| 116 | .where((c) => cryptoCurrencyOrTokenToWalletType(c) == _selectedNetwork) |
| 117 | .toList(growable: false); |
| 118 | if (filtered.isNotEmpty) variants = filtered; |
| 119 | } |
| 120 | |
| 121 | if (variants.length <= 1) { |
| 122 | _selectCurrency(variants.isNotEmpty ? variants.first : tapped); |
| 123 | return; |
| 124 | } |
| 125 | |
| 126 | Navigator.of(context).push( |
| 127 | PageRouteBuilder<void>( |
| 128 | opaque: true, |
| 129 | transitionDuration: const Duration(milliseconds: 250), |
| 130 | reverseTransitionDuration: const Duration(milliseconds: 250), |
| 131 | pageBuilder: (_, __, ___) => SelectNetworkPage( |
| 132 | assetTitle: tapped.title, |
| 133 | assetFullName: tapped.fullName, |
| 134 | assetIconPath: tapped.iconPath, |
| 135 | variants: variants, |
| 136 | onSelected: widget.args.onSelected, |
| 137 | ), |
| 138 | transitionsBuilder: (_, animation, __, child) => SlideTransition( |
| 139 | position: animation.drive( |
| 140 | Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero) |
| 141 | .chain(CurveTween(curve: Curves.easeInOut)), |
| 142 | ), |
| 143 | child: child, |
| 144 | ), |
| 145 | ), |
| 146 | ); |
| 147 | } |
| 148 | |
| 149 | @override |
| 150 | Widget build(BuildContext context) { |
| 151 | return Column( |
| 152 | mainAxisSize: MainAxisSize.max, |
| 153 | children: [ |
| 154 | if (_networks.length > 1) |
| 155 | ChainChipStrip( |
| 156 | walletTypes: _networks, |
| 157 | selected: _selectedNetwork, |
| 158 | onSelected: (network) => setState(() => _selectedNetwork = network), |
| 159 | ), |
| 160 | Expanded( |
| 161 | child: _MultiNetworkPickerBody( |
| 162 | items: _visibleItems, |
| 163 | isSearching: _isSearching, |
| 164 | recents: _recents, |
| 165 | recentsLoaded: _recentsLoaded, |
| 166 | natives: _natives, |
| 167 | selected: widget.args.selected, |
| 168 | symbolResolver: widget.args.symbolResolver, |
| 169 | onSelect: _selectCurrency, |
| 170 | onStablecoinTap: _onStablecoinPillTapped, |
| 171 | ), |
| 172 | ), |
| 173 | CurrencyPickerSearchField( |
| 174 | controller: _searchController, |
| 175 | hintText: S.of(context).search, |
| 176 | ), |
| 177 | ], |
| 178 | ); |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | class _MultiNetworkPickerBody extends StatefulWidget { |
| 183 | const _MultiNetworkPickerBody({ |
| 184 | required this.items, |
| 185 | required this.isSearching, |
| 186 | required this.recents, |
| 187 | required this.recentsLoaded, |
| 188 | required this.natives, |
| 189 | required this.selected, |
| 190 | required this.symbolResolver, |
| 191 | required this.onSelect, |
| 192 | required this.onStablecoinTap, |
| 193 | }); |
| 194 | |
| 195 | final bool isSearching; |
| 196 | final bool recentsLoaded; |
| 197 | final CryptoCurrency? selected; |
| 198 | final List<CryptoCurrency> items; |
| 199 | final List<CryptoCurrency> recents; |
| 200 | final Set<CryptoCurrency> natives; |
| 201 | final void Function(CryptoCurrency) onSelect; |
| 202 | final String Function(CryptoCurrency) symbolResolver; |
| 203 | final void Function(CryptoCurrency) onStablecoinTap; |
| 204 | |
| 205 | @override |
| 206 | State<_MultiNetworkPickerBody> createState() => _MultiNetworkPickerBodyState(); |
| 207 | } |
| 208 | |
| 209 | class _MultiNetworkPickerBodyState extends State<_MultiNetworkPickerBody> { |
| 210 | static const int _previewCount = 3; |
| 211 | |
| 212 | final ScrollController _scrollController = ScrollController(); |
| 213 | bool _moreCryptosSectionExpanded = false; |
| 214 | bool _xstocksSectionExpanded = false; |
| 215 | |
| 216 | @override |
| 217 | void initState() { |
| 218 | super.initState(); |
| 219 | final selected = widget.selected; |
| 220 | if (selected == null) return; |
| 221 | |
| 222 | final moreCryptos = _computeMoreCryptosSection(widget.items); |
| 223 | final xstocks = _computeXstocksSection(widget.items); |
| 224 | |
| 225 | if (moreCryptos.indexOf(selected) >= _previewCount) { |
| 226 | _moreCryptosSectionExpanded = true; |
| 227 | } |
| 228 | |
| 229 | if (xstocks.indexOf(selected) >= _previewCount) { |
| 230 | _xstocksSectionExpanded = true; |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | @override |
| 235 | void dispose() { |
| 236 | _scrollController.dispose(); |
| 237 | super.dispose(); |
| 238 | } |
| 239 | |
| 240 | List<CryptoCurrency> _computeMoreCryptosSection(List<CryptoCurrency> from) { |
| 241 | final list = from |
| 242 | .where((c) => |
| 243 | !widget.natives.contains(c) && |
| 244 | cryptoCurrencyOrTokenToWalletType(c) == null && |
| 245 | c.tag == null && |
| 246 | !c.groups.contains(CurrencyGroups.stablecoin) && |
| 247 | !c.groups.contains(CurrencyGroups.tokenizedStock)) |
| 248 | .toList(); |
| 249 | |
| 250 | list.sort((a, b) => (a.fullName ?? a.title).toLowerCase().compareTo( |
| 251 | (b.fullName ?? b.title).toLowerCase(), |
| 252 | )); |
| 253 | |
| 254 | return list; |
| 255 | } |
| 256 | |
| 257 | List<CryptoCurrency> _computeXstocksSection(List<CryptoCurrency> from) { |
| 258 | return from |
| 259 | .where((c) => c.groups.contains(CurrencyGroups.tokenizedStock)) |
| 260 | .toList(growable: false); |
| 261 | } |
| 262 | |
| 263 | @override |
| 264 | Widget build(BuildContext context) { |
| 265 | final items = widget.items; |
| 266 | final isSearching = widget.isSearching; |
| 267 | final recents = widget.recents; |
| 268 | final recentsLoaded = widget.recentsLoaded; |
| 269 | final natives = widget.natives; |
| 270 | final selected = widget.selected; |
| 271 | final symbolResolver = widget.symbolResolver; |
| 272 | final onSelect = widget.onSelect; |
| 273 | final onStablecoinTap = widget.onStablecoinTap; |
| 274 | |
| 275 | if (items.isEmpty) { |
| 276 | return Center( |
| 277 | child: Padding( |
| 278 | padding: const EdgeInsets.all(24), |
| 279 | child: Text( |
| 280 | S.of(context).picker_no_matches, |
| 281 | textAlign: TextAlign.center, |
| 282 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( |
| 283 | color: Theme.of(context).colorScheme.onSurfaceVariant, |
| 284 | ), |
| 285 | ), |
| 286 | ), |
| 287 | ); |
| 288 | } |
| 289 | |
| 290 | if (isSearching) { |
| 291 | return NotificationListener<ScrollNotification>( |
| 292 | onNotification: (_) => true, |
| 293 | child: ListView( |
| 294 | controller: _scrollController, |
| 295 | primary: false, |
| 296 | physics: const ClampingScrollPhysics(), |
| 297 | padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), |
| 298 | children: [ |
| 299 | CurrencyPickerListContainer( |
| 300 | rows: [ |
| 301 | for (final item in items) |
| 302 | CurrencyPickerRow( |
| 303 | currency: item, |
| 304 | isSelected: selected != null && selected == item, |
| 305 | chainPillLabel: _chainPillLabelFor(item), |
| 306 | chainBadgePath: _chainBadgePathFor(item), |
| 307 | trailing: _SymbolTrailing( |
| 308 | currency: item, |
| 309 | symbolResolver: symbolResolver, |
| 310 | ), |
| 311 | onTap: () => onSelect(item), |
| 312 | ), |
| 313 | ], |
| 314 | ), |
| 315 | ], |
| 316 | ), |
| 317 | ); |
| 318 | } |
| 319 | |
| 320 | final visibleRecents = recents.where(items.contains).toList(growable: false); |
| 321 | |
| 322 | final seenStablecoinTitles = <String>{}; |
| 323 | final stablecoins = items |
| 324 | .where((c) => isTrustedStablecoin(c) && seenStablecoinTitles.add(c.title.toUpperCase())) |
| 325 | .toList(growable: false); |
| 326 | |
| 327 | final cryptocurrencies = items.where(natives.contains).toList(growable: false); |
| 328 | final moreCryptos = _computeMoreCryptosSection(items); |
| 329 | final xstocks = _computeXstocksSection(items); |
| 330 | final allAssets = [...items]..sort((a, b) => (a.fullName ?? a.title).toLowerCase().compareTo( |
| 331 | (b.fullName ?? b.title).toLowerCase(), |
| 332 | )); |
| 333 | |
| 334 | final section = _selectedSection( |
| 335 | recents: visibleRecents, |
| 336 | stablecoins: stablecoins, |
| 337 | cryptocurrencies: cryptocurrencies, |
| 338 | moreCryptos: moreCryptos, |
| 339 | xstocks: xstocks, |
| 340 | ); |
| 341 | |
| 342 | final moreCryptosVisible = _moreCryptosSectionExpanded |
| 343 | ? moreCryptos |
| 344 | : moreCryptos.take(_previewCount).toList(growable: false); |
| 345 | final xstocksVisible = |
| 346 | _xstocksSectionExpanded ? xstocks : xstocks.take(_previewCount).toList(growable: false); |
| 347 | |
| 348 | return NotificationListener<ScrollNotification>( |
| 349 | onNotification: (_) => true, |
| 350 | child: ListView( |
| 351 | controller: _scrollController, |
| 352 | primary: false, |
| 353 | physics: const ClampingScrollPhysics(), |
| 354 | padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), |
| 355 | children: [ |
| 356 | if (recentsLoaded && visibleRecents.isNotEmpty) |
| 357 | _PickerSection( |
| 358 | title: S.of(context).picker_section_recents, |
| 359 | child: _RecentsRow( |
| 360 | items: visibleRecents, |
| 361 | selected: section == _SelSection.recents ? selected : null, |
| 362 | symbolResolver: symbolResolver, |
| 363 | onTap: onSelect, |
| 364 | ), |
| 365 | ), |
| 366 | if (stablecoins.isNotEmpty) |
| 367 | _PickerSection( |
| 368 | title: S.of(context).picker_section_stablecoins, |
| 369 | child: PillGrid( |
| 370 | items: stablecoins, |
| 371 | selected: section == _SelSection.stablecoins ? selected : null, |
| 372 | onTap: onStablecoinTap, |
| 373 | symbolResolver: symbolResolver, |
| 374 | ), |
| 375 | ), |
| 376 | if (cryptocurrencies.isNotEmpty) |
| 377 | _PickerSection( |
| 378 | title: S.of(context).picker_section_cryptocurrencies, |
| 379 | child: CurrencyPickerListContainer( |
| 380 | rows: [ |
| 381 | for (final item in cryptocurrencies) |
| 382 | CurrencyPickerRow( |
| 383 | currency: item, |
| 384 | isSelected: section == _SelSection.cryptocurrencies && selected == item, |
| 385 | chainPillLabel: _chainPillLabelFor(item), |
| 386 | chainBadgePath: _chainBadgePathFor(item), |
| 387 | trailing: _SymbolTrailing( |
| 388 | currency: item, |
| 389 | symbolResolver: symbolResolver, |
| 390 | ), |
| 391 | onTap: () => onSelect(item), |
| 392 | ), |
| 393 | ], |
| 394 | ), |
| 395 | ), |
| 396 | if (moreCryptos.isNotEmpty) |
| 397 | _PickerSection( |
| 398 | title: S.of(context).picker_section_more_cryptocurrencies, |
| 399 | child: CurrencyPickerListContainer( |
| 400 | rows: [ |
| 401 | for (final item in moreCryptosVisible) |
| 402 | CurrencyPickerRow( |
| 403 | currency: item, |
| 404 | isSelected: section == _SelSection.moreCryptocurrencies && selected == item, |
| 405 | trailing: _SymbolTrailing( |
| 406 | currency: item, |
| 407 | symbolResolver: symbolResolver, |
| 408 | ), |
| 409 | onTap: () => onSelect(item), |
| 410 | ), |
| 411 | if (moreCryptos.length > _previewCount) |
| 412 | _SeeAllRow( |
| 413 | expanded: _moreCryptosSectionExpanded, |
| 414 | onTap: () => setState( |
| 415 | () => _moreCryptosSectionExpanded = !_moreCryptosSectionExpanded), |
| 416 | ), |
| 417 | ], |
| 418 | ), |
| 419 | ), |
| 420 | if (xstocks.isNotEmpty) |
| 421 | _PickerSection( |
| 422 | title: S.of(context).picker_section_tokenized_stocks, |
| 423 | child: CurrencyPickerListContainer( |
| 424 | rows: [ |
| 425 | for (final item in xstocksVisible) |
| 426 | CurrencyPickerRow( |
| 427 | currency: item, |
| 428 | isSelected: section == _SelSection.xstocks && selected == item, |
| 429 | trailing: _SymbolTrailing( |
| 430 | currency: item, |
| 431 | symbolResolver: symbolResolver, |
| 432 | ), |
| 433 | onTap: () => onSelect(item), |
| 434 | chainPillLabel: _chainPillLabelFor(item), |
| 435 | chainBadgePath: _chainBadgePathFor(item), |
| 436 | ), |
| 437 | if (xstocks.length > _previewCount) |
| 438 | _SeeAllRow( |
| 439 | expanded: _xstocksSectionExpanded, |
| 440 | onTap: () => |
| 441 | setState(() => _xstocksSectionExpanded = !_xstocksSectionExpanded), |
| 442 | ), |
| 443 | ], |
| 444 | ), |
| 445 | ), |
| 446 | if (allAssets.isNotEmpty) |
| 447 | _PickerSection( |
| 448 | title: S.of(context).picker_section_all_assets, |
| 449 | child: CurrencyPickerListContainer( |
| 450 | rows: [ |
| 451 | for (final item in allAssets) |
| 452 | CurrencyPickerRow( |
| 453 | currency: item, |
| 454 | isSelected: section == _SelSection.allAssets && selected == item, |
| 455 | chainPillLabel: _chainPillLabelFor(item), |
| 456 | chainBadgePath: _chainBadgePathFor(item), |
| 457 | trailing: _SymbolTrailing( |
| 458 | currency: item, |
| 459 | symbolResolver: symbolResolver, |
| 460 | ), |
| 461 | onTap: () => onSelect(item), |
| 462 | ), |
| 463 | ], |
| 464 | ), |
| 465 | ), |
| 466 | ], |
| 467 | ), |
| 468 | ); |
| 469 | } |
| 470 | |
| 471 | String? _chainPillLabelFor(CryptoCurrency c) { |
| 472 | if (c == CryptoCurrency.btcln) return _shortChainLabel(c); |
| 473 | if (_isL2NativeEth(c)) return _shortChainLabel(c); |
| 474 | if (widget.natives.contains(c)) return null; |
| 475 | if (cryptoCurrencyOrTokenToWalletType(c) == null) return null; |
| 476 | return _shortChainLabel(c); |
| 477 | } |
| 478 | |
| 479 | String _shortChainLabel(CryptoCurrency c) { |
| 480 | if (cryptoCurrencyOrTokenToWalletType(c) == WalletType.bsc) return 'BSC'; |
| 481 | return chainNameForCurrency(c); |
| 482 | } |
| 483 | |
| 484 | String? _chainBadgePathFor(CryptoCurrency c) { |
| 485 | if (_isL2NativeEth(c)) return c.chainIconPath; |
| 486 | if (widget.natives.contains(c)) return null; |
| 487 | final wt = cryptoCurrencyOrTokenToWalletType(c); |
| 488 | if (wt == null) return null; |
| 489 | return c.chainIconPath ?? walletTypeToCryptoCurrency(wt).chainIconPath; |
| 490 | } |
| 491 | |
| 492 | bool _isL2NativeEth(CryptoCurrency c) => |
| 493 | c == CryptoCurrency.arbEth || c == CryptoCurrency.baseEth; |
| 494 | |
| 495 | _SelSection? _selectedSection({ |
| 496 | required List<CryptoCurrency> recents, |
| 497 | required List<CryptoCurrency> stablecoins, |
| 498 | required List<CryptoCurrency> cryptocurrencies, |
| 499 | required List<CryptoCurrency> moreCryptos, |
| 500 | required List<CryptoCurrency> xstocks, |
| 501 | }) { |
| 502 | final s = widget.selected; |
| 503 | if (s == null) return null; |
| 504 | if (recents.contains(s)) return _SelSection.recents; |
| 505 | if (stablecoins.any((c) => c.title.toUpperCase() == s.title.toUpperCase())) { |
| 506 | return _SelSection.stablecoins; |
| 507 | } |
| 508 | if (cryptocurrencies.contains(s)) return _SelSection.cryptocurrencies; |
| 509 | if (moreCryptos.contains(s)) return _SelSection.moreCryptocurrencies; |
| 510 | if (xstocks.contains(s)) return _SelSection.xstocks; |
| 511 | return _SelSection.allAssets; |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | class _PickerSection extends StatelessWidget { |
| 516 | const _PickerSection({required this.title, required this.child}); |
| 517 | |
| 518 | final String title; |
| 519 | final Widget child; |
| 520 | |
| 521 | @override |
| 522 | Widget build(BuildContext context) { |
| 523 | return Column( |
| 524 | crossAxisAlignment: CrossAxisAlignment.start, |
| 525 | children: [ |
| 526 | PickerSectionHeader(title: title), |
| 527 | child, |
| 528 | const SizedBox(height: 16), |
| 529 | ], |
| 530 | ); |
| 531 | } |
| 532 | } |
| 533 | |
| 534 | class _SymbolTrailing extends StatelessWidget { |
| 535 | const _SymbolTrailing({ |
| 536 | required this.currency, |
| 537 | required this.symbolResolver, |
| 538 | }); |
| 539 | |
| 540 | final CryptoCurrency currency; |
| 541 | final String Function(CryptoCurrency) symbolResolver; |
| 542 | |
| 543 | @override |
| 544 | Widget build(BuildContext context) { |
| 545 | return Text( |
| 546 | symbolResolver(currency), |
| 547 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( |
| 548 | fontWeight: FontWeight.w500, |
| 549 | color: Theme.of(context).colorScheme.onSurfaceVariant, |
| 550 | ), |
| 551 | ); |
| 552 | } |
| 553 | } |
| 554 | |
| 555 | class _SeeAllRow extends StatelessWidget { |
| 556 | const _SeeAllRow({ |
| 557 | required this.expanded, |
| 558 | required this.onTap, |
| 559 | }); |
| 560 | |
| 561 | final bool expanded; |
| 562 | final VoidCallback onTap; |
| 563 | |
| 564 | @override |
| 565 | Widget build(BuildContext context) { |
| 566 | final colors = Theme.of(context).colorScheme; |
| 567 | final label = expanded ? S.of(context).picker_show_less : S.of(context).picker_see_all; |
| 568 | return InkWell( |
| 569 | onTap: onTap, |
| 570 | child: Padding( |
| 571 | padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), |
| 572 | child: Row( |
| 573 | children: [ |
| 574 | Expanded( |
| 575 | child: Text( |
| 576 | label, |
| 577 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( |
| 578 | fontWeight: FontWeight.w500, |
| 579 | color: colors.primary, |
| 580 | ), |
| 581 | ), |
| 582 | ), |
| 583 | ], |
| 584 | ), |
| 585 | ), |
| 586 | ); |
| 587 | } |
| 588 | } |
| 589 | |
| 590 | class _RecentsRow extends StatelessWidget { |
| 591 | const _RecentsRow({ |
| 592 | required this.items, |
| 593 | required this.selected, |
| 594 | required this.symbolResolver, |
| 595 | required this.onTap, |
| 596 | }); |
| 597 | |
| 598 | final List<CryptoCurrency> items; |
| 599 | final CryptoCurrency? selected; |
| 600 | final String Function(CryptoCurrency) symbolResolver; |
| 601 | final void Function(CryptoCurrency) onTap; |
| 602 | |
| 603 | @override |
| 604 | Widget build(BuildContext context) { |
| 605 | return SizedBox( |
| 606 | height: 44, |
| 607 | child: ListView.separated( |
| 608 | scrollDirection: Axis.horizontal, |
| 609 | itemCount: items.length, |
| 610 | separatorBuilder: (_, __) => const SizedBox(width: 8), |
| 611 | itemBuilder: (_, i) { |
| 612 | final item = items[i]; |
| 613 | return _RecentPill( |
| 614 | currency: item, |
| 615 | label: symbolResolver(item), |
| 616 | isSelected: selected != null && selected == item, |
| 617 | onTap: () => onTap(item), |
| 618 | ); |
| 619 | }, |
| 620 | ), |
| 621 | ); |
| 622 | } |
| 623 | } |
| 624 | |
| 625 | class _RecentPill extends StatelessWidget { |
| 626 | const _RecentPill({ |
| 627 | required this.currency, |
| 628 | required this.label, |
| 629 | required this.isSelected, |
| 630 | required this.onTap, |
| 631 | }); |
| 632 | |
| 633 | final CryptoCurrency currency; |
| 634 | final String label; |
| 635 | final bool isSelected; |
| 636 | final VoidCallback onTap; |
| 637 | |
| 638 | @override |
| 639 | Widget build(BuildContext context) { |
| 640 | final colors = Theme.of(context).colorScheme; |
| 641 | return InkWell( |
| 642 | onTap: onTap, |
| 643 | borderRadius: BorderRadius.circular(80), |
| 644 | child: Container( |
| 645 | padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), |
| 646 | decoration: BoxDecoration( |
| 647 | color: colors.surfaceContainer, |
| 648 | borderRadius: BorderRadius.circular(80), |
| 649 | border: isSelected ? Border.all(color: colors.primary, width: 1.5) : null, |
| 650 | ), |
| 651 | child: Row( |
| 652 | mainAxisSize: MainAxisSize.min, |
| 653 | children: [ |
| 654 | TokenImageWidget( |
| 655 | imageUrl: currency.iconPath ?? '', |
| 656 | size: 24, |
| 657 | ), |
| 658 | const SizedBox(width: 8), |
| 659 | Text( |
| 660 | label, |
| 661 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( |
| 662 | fontWeight: FontWeight.w600, |
| 663 | ), |
| 664 | ), |
| 665 | ], |
| 666 | ), |
| 667 | ), |
| 668 | ); |
| 669 | } |
| 670 | } |
| 671 | |
| 672 | enum _SelSection { |
| 673 | recents, |
| 674 | stablecoins, |
| 675 | cryptocurrencies, |
| 676 | moreCryptocurrencies, |
| 677 | xstocks, |
| 678 | allAssets, |
| 679 | } |