/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. * Generated by Cypher Stack on 2023-05-26 * */ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import '../../app_config.dart'; import '../../models/add_wallet_list_entity/sub_classes/coin_entity.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_wallet_card.dart'; import '../../providers/db/main_db_provider.dart'; import '../../providers/providers.dart'; import '../../services/event_bus/events/wallet_added_event.dart'; import '../../services/event_bus/global_event_bus.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/models/wallet_info.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/wallet.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/icon_widgets/x_icon.dart'; import '../../widgets/master_wallet_card.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_text_field.dart'; import '../../widgets/textfield_icon_button.dart'; import '../../widgets/wallet_card.dart'; import '../add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; class WalletsOverview extends ConsumerStatefulWidget { const WalletsOverview({ super.key, required this.coin, this.navigatorState, this.overrideSimpleWalletCardPopPreviousValueWith, }); final CryptoCurrency coin; final NavigatorState? navigatorState; final bool? overrideSimpleWalletCardPopPreviousValueWith; static const routeName = "/walletsOverview"; @override ConsumerState createState() => _EthWalletsOverviewState(); } typedef WalletListItemData = ({Wallet wallet, List contracts}); class _EthWalletsOverviewState extends ConsumerState { final isDesktop = Util.isDesktop; late final TextEditingController _searchController; late final FocusNode searchFieldFocusNode; String _searchString = ""; final Map wallets = {}; List _filter(String searchTerm) { // clean out deleted wallets final existingWalletIds = ref .read(mainDBProvider) .isar .walletInfo .where() .walletIdProperty() .findAllSync(); wallets.removeWhere((k, v) => !existingWalletIds.contains(k)); if (searchTerm.isEmpty) { return wallets.values.toList() ..sort((a, b) => a.wallet.info.name.compareTo(b.wallet.info.name)); } final Map results = {}; final term = searchTerm.toLowerCase(); for (final entry in wallets.entries) { bool includeManager = false; // search wallet name and total balance includeManager |= _elementContains(entry.value.wallet.info.name, term); includeManager |= _elementContains( entry.value.wallet.info.cachedBalance.total.decimal.toString(), term, ); final List contracts = []; for (final contract in entry.value.contracts) { if (_elementContains(contract.name, term)) { contracts.add(contract); } else if (_elementContains(contract.symbol, term)) { contracts.add(contract); } else if (_elementContains(contract.type.name, term)) { contracts.add(contract); } else if (_elementContains(contract.address, term)) { contracts.add(contract); } } if (includeManager || contracts.isNotEmpty) { results.addEntries([entry]); } } return results.values.toList() ..sort((a, b) => a.wallet.info.name.compareTo(b.wallet.info.name)); } bool _elementContains(String element, String term) { return element.toLowerCase().contains(term); } void updateWallets() { final walletsData = ref.read(mainDBProvider).isar.walletInfo.where().findAllSync(); walletsData.removeWhere((e) => e.coin != widget.coin); if (widget.coin is Ethereum) { for (final data in walletsData) { final List contracts = []; final contractAddresses = ref.read(pWalletTokenAddresses(data.walletId)); // fetch each contract for (final contractAddress in contractAddresses) { final contract = ref .read( mainDBProvider, ) .getEthContractSync( contractAddress, ); // add it to list if it exists in DB if (contract != null) { contracts.add(contract); } } // add tuple to list wallets[data.walletId] = ( wallet: ref.read(pWallets).getWallet( data.walletId, ), contracts: contracts, ); } } else { // add non token wallet tuple to list for (final data in walletsData) { // desktop single coin apps may cause issues so lets just ignore the error and move on try { wallets[data.walletId] = ( wallet: ref.read(pWallets).getWallet( data.walletId, ), contracts: [], ); } catch (_) { // lol bandaid for single coin based apps } } } } @override void initState() { _searchController = TextEditingController(); searchFieldFocusNode = FocusNode(); updateWallets(); if (AppConfig.isSingleCoinApp) { GlobalEventBus.instance.on().listen((_) { updateWallets(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() {}); } }); }); } super.initState(); } @override void dispose() { _searchController.dispose(); searchFieldFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ConditionalParent( condition: !isDesktop && !AppConfig.isSingleCoinApp, builder: (child) => Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( leading: const AppBarBackButton(), title: Text( "${widget.coin.prettyName} (${widget.coin.ticker}) wallets", style: STextStyles.navBarTitle(context), ), actions: [ AspectRatio( aspectRatio: 1, child: AppBarIconButton( icon: SvgPicture.asset( Assets.svg.plus, width: 18, height: 18, color: Theme.of(context) .extension()! .topNavIconPrimary, ), onPressed: () { Navigator.of(context).pushNamed( CreateOrRestoreWalletView.routeName, arguments: CoinEntity(widget.coin), ); }, ), ), ], ), body: SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: child, ), ), ), ), child: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), child: TextField( autocorrect: !isDesktop, enableSuggestions: !isDesktop, controller: _searchController, focusNode: searchFieldFocusNode, onChanged: (value) { setState(() { _searchString = value; }); }, style: isDesktop ? STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of(context) .extension()! .textFieldActiveText, height: 1.8, ) : STextStyles.field(context), decoration: standardInputDecoration( "Search...", searchFieldFocusNode, context, desktopMed: isDesktop, ).copyWith( prefixIcon: Padding( padding: EdgeInsets.symmetric( horizontal: isDesktop ? 12 : 10, vertical: isDesktop ? 18 : 16, ), child: SvgPicture.asset( Assets.svg.search, width: isDesktop ? 20 : 16, height: isDesktop ? 20 : 16, ), ), suffixIcon: _searchController.text.isNotEmpty ? Padding( padding: const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( children: [ TextFieldIconButton( child: const XIcon(), onTap: () async { setState(() { _searchController.text = ""; _searchString = ""; }); }, ), ], ), ), ) : null, ), ), ), const SizedBox( height: 16, ), Expanded( child: Builder( builder: (context) { final data = _filter(_searchString); return ListView.separated( itemBuilder: (_, index) { final entry = data[index]; final wallet = entry.wallet; if (wallet.cryptoCurrency.hasTokenSupport) { if (isDesktop) { return DesktopExpandingWalletCard( key: Key( "${wallet.walletId}_${entry.contracts.map((e) => e.address).join()}", ), data: entry, navigatorState: widget.navigatorState!, ); } else { return MasterWalletCard( key: Key(wallet.walletId), walletId: wallet.walletId, ); } } else { return ConditionalParent( key: Key(wallet.walletId), condition: isDesktop, builder: (child) => RoundedWhiteContainer( padding: const EdgeInsets.symmetric( vertical: 14, horizontal: 20, ), borderColor: Theme.of(context) .extension()! .backgroundAppBar, child: child, ), child: SimpleWalletCard( walletId: wallet.walletId, popPrevious: widget .overrideSimpleWalletCardPopPreviousValueWith == null ? isDesktop : widget .overrideSimpleWalletCardPopPreviousValueWith!, desktopNavigatorState: isDesktop ? widget.navigatorState : null, ), ); } }, separatorBuilder: (_, __) => SizedBox( height: isDesktop ? 10 : 8, ), itemCount: data.length, ); }, ), ), ], ), ); } }