diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index 7f739b938..0eeff1089 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -19,13 +19,18 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/app_bar_field.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/dropdown_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/expandable2.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/toggle.dart'; import 'package:tuple/tuple.dart'; +import '../../widgets/animated_widgets/rotate_icon.dart'; +import '../../widgets/rounded_container.dart'; + enum CoinControlViewType { manage, use; @@ -60,6 +65,9 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> { CCSortDescriptor _sort = CCSortDescriptor.age; + Map<String, List<Id>>? _map; + List<Id>? _list; + final Set<UTXO> _selectedAvailable = {}; final Set<UTXO> _selectedBlocked = {}; @@ -115,17 +123,29 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> { ), ); - final ids = MainDB.instance.queryUTXOsSync( - walletId: widget.walletId, - filter: _isSearching - ? CCFilter.all - : _showBlocked - ? CCFilter.frozen - : CCFilter.available, - sort: _sort, - searchTerm: _isSearching ? searchController.text : "", - coin: coin, - ); + if (_sort == CCSortDescriptor.address && !_isSearching) { + _list = null; + _map = MainDB.instance.queryUTXOsGroupedByAddressSync( + walletId: widget.walletId, + filter: CCFilter.all, + sort: _sort, + searchTerm: "", + coin: coin, + ); + } else { + _map = null; + _list = MainDB.instance.queryUTXOsSync( + walletId: widget.walletId, + filter: _isSearching + ? CCFilter.all + : _showBlocked + ? CCFilter.frozen + : CCFilter.available, + sort: _sort, + searchTerm: _isSearching ? searchController.text : "", + coin: coin, + ); + } return WillPopScope( onWillPop: () async { @@ -223,22 +243,18 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> { ), AspectRatio( aspectRatio: 1, - child: AppBarIconButton( - size: 36, - icon: SvgPicture.asset( - Assets.svg.verticalEllipsis, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .topNavIconPrimary, - ), - onPressed: () { - // show search - setState(() { - _isSearching = true; - }); + child: JDropdownIconButton( + mobileAppBar: true, + groupValue: _sort, + items: CCSortDescriptor.values.toSet(), + onSelectionChanged: (CCSortDescriptor? newValue) { + if (newValue != null && newValue != _sort) { + setState(() { + _sort = newValue; + }); + } }, + displayPrefix: "Sort by", ), ), ], @@ -273,7 +289,7 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> { const SizedBox( height: 10, ), - if (!_isSearching) + if (!(_isSearching || _map != null)) SizedBox( height: 48, child: Toggle( @@ -307,14 +323,14 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> { if (_isSearching) Expanded( child: ListView.separated( - itemCount: ids.length, + itemCount: _list!.length, separatorBuilder: (context, _) => const SizedBox( height: 10, ), itemBuilder: (context, index) { final utxo = MainDB.instance.isar.utxos .where() - .idEqualTo(ids[index]) + .idEqualTo(_list![index]) .findFirstSync()!; final isSelected = @@ -365,65 +381,219 @@ class _CoinControlViewState extends ConsumerState<CoinControlView> { ), ), if (!_isSearching) - Expanded( - child: ListView.separated( - itemCount: ids.length, - separatorBuilder: (context, _) => const SizedBox( - height: 10, - ), - itemBuilder: (context, index) { - final utxo = MainDB.instance.isar.utxos - .where() - .idEqualTo(ids[index]) - .findFirstSync()!; + _list != null + ? Expanded( + child: ListView.separated( + itemCount: _list!.length, + separatorBuilder: (context, _) => + const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; - final isSelected = _showBlocked - ? _selectedBlocked.contains(utxo) - : _selectedAvailable.contains(utxo); + final isSelected = _showBlocked + ? _selectedBlocked.contains(utxo) + : _selectedAvailable.contains(utxo); - return UtxoCard( - key: Key( - "${utxo.walletId}_${utxo.id}_$isSelected"), - walletId: widget.walletId, - utxo: utxo, - canSelect: widget.type == - CoinControlViewType.manage || - (widget.type == CoinControlViewType.use && - !_showBlocked && - utxo.isConfirmed( - currentChainHeight, - coin.requiredConfirmations, - )), - initialSelectedState: isSelected, - onSelectedChanged: (value) { - if (value) { - _showBlocked - ? _selectedBlocked.add(utxo) - : _selectedAvailable.add(utxo); - } else { - _showBlocked - ? _selectedBlocked.remove(utxo) - : _selectedAvailable.remove(utxo); - } - setState(() {}); - }, - onPressed: () async { - final result = - await Navigator.of(context).pushNamed( - UtxoDetailsView.routeName, - arguments: Tuple2( - utxo.id, - widget.walletId, - ), - ); - if (mounted && result == "refresh") { - setState(() {}); - } - }, - ); - }, - ), - ), + return UtxoCard( + key: Key( + "${utxo.walletId}_${utxo.id}_$isSelected"), + walletId: widget.walletId, + utxo: utxo, + canSelect: widget.type == + CoinControlViewType.manage || + (widget.type == + CoinControlViewType.use && + !_showBlocked && + utxo.isConfirmed( + currentChainHeight, + coin.requiredConfirmations, + )), + initialSelectedState: isSelected, + onSelectedChanged: (value) { + if (value) { + _showBlocked + ? _selectedBlocked.add(utxo) + : _selectedAvailable.add(utxo); + } else { + _showBlocked + ? _selectedBlocked.remove(utxo) + : _selectedAvailable + .remove(utxo); + } + setState(() {}); + }, + onPressed: () async { + final result = + await Navigator.of(context) + .pushNamed( + UtxoDetailsView.routeName, + arguments: Tuple2( + utxo.id, + widget.walletId, + ), + ); + if (mounted && result == "refresh") { + setState(() {}); + } + }, + ); + }, + ), + ) + : Expanded( + child: ListView.separated( + itemCount: _map!.entries.length, + separatorBuilder: (context, _) => + const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + final entry = + _map!.entries.elementAt(index); + final _controller = + RotateIconController(); + + return Expandable2( + border: Theme.of(context) + .extension<StackColors>()! + .backgroundAppBar, + background: Theme.of(context) + .extension<StackColors>()! + .popupBG, + animationDurationMultiplier: + 0.2 * entry.value.length, + onExpandWillChange: (state) { + if (state == + Expandable2State.expanded) { + _controller.forward?.call(); + } else { + _controller.reverse?.call(); + } + }, + header: RoundedContainer( + padding: const EdgeInsets.all(14), + color: Colors.transparent, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + entry.key, + style: + STextStyles.w600_14( + context), + ), + const SizedBox( + height: 2, + ), + Text( + "${entry.value.length} " + "output${entry.value.length > 1 ? "s" : ""}", + style: + STextStyles.w500_12( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textSubtitle1, + ), + ), + ], + ), + ), + RotateIcon( + animationDurationMultiplier: + 0.2 * entry.value.length, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + color: Theme.of(context) + .extension<StackColors>()! + .textSubtitle1, + ), + curve: Curves.easeInOut, + controller: _controller, + ), + ], + ), + ), + children: entry.value.map( + (id) { + final utxo = MainDB + .instance.isar.utxos + .where() + .idEqualTo(id) + .findFirstSync()!; + + final isSelected = _selectedBlocked + .contains(utxo) || + _selectedAvailable + .contains(utxo); + + return UtxoCard( + key: Key( + "${utxo.walletId}_${utxo.id}_$isSelected"), + walletId: widget.walletId, + utxo: utxo, + canSelect: widget.type == + CoinControlViewType + .manage || + (widget.type == + CoinControlViewType + .use && + !utxo.isBlocked && + utxo.isConfirmed( + currentChainHeight, + coin.requiredConfirmations, + )), + initialSelectedState: isSelected, + onSelectedChanged: (value) { + if (value) { + utxo.isBlocked + ? _selectedBlocked + .add(utxo) + : _selectedAvailable + .add(utxo); + } else { + utxo.isBlocked + ? _selectedBlocked + .remove(utxo) + : _selectedAvailable + .remove(utxo); + } + setState(() {}); + }, + onPressed: () async { + final result = + await Navigator.of(context) + .pushNamed( + UtxoDetailsView.routeName, + arguments: Tuple2( + utxo.id, + widget.walletId, + ), + ); + if (mounted && + result == "refresh") { + setState(() {}); + } + }, + ); + }, + ).toList(), + ); + }, + ), + ), ], ), ), diff --git a/lib/widgets/custom_buttons/dropdown_button.dart b/lib/widgets/custom_buttons/dropdown_button.dart index b31d8e2ed..d639ccf54 100644 --- a/lib/widgets/custom_buttons/dropdown_button.dart +++ b/lib/widgets/custom_buttons/dropdown_button.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -132,12 +133,14 @@ class JDropdownIconButton<T> extends StatefulWidget { this.onSelectionChanged, this.groupValue, this.redrawOnScreenSizeChanged = false, + this.mobileAppBar = false, }) : super(key: key); final String displayPrefix; final void Function(T?)? onSelectionChanged; final T? groupValue; final Set<T> items; + final bool mobileAppBar; /// setting this to true should be done carefully final bool redrawOnScreenSizeChanged; @@ -211,37 +214,51 @@ class _JDropdownIconButtonState<T> extends State<JDropdownIconButton<T>> { }); } - return SizedBox( - key: _key, - height: 56, - width: 56, - child: TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getSecondaryEnabledButtonStyle(context) - ?.copyWith( - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - side: BorderSide( - color: Theme.of(context) - .extension<StackColors>()! - .buttonBackBorderSecondary, - width: 1, - ), - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - ), - onPressed: _isOpen ? close : open, - child: SvgPicture.asset( + if (widget.mobileAppBar) { + return AppBarIconButton( + key: _key, + size: 36, + icon: SvgPicture.asset( Assets.svg.list, width: 20, height: 20, + color: Theme.of(context).extension<StackColors>()!.topNavIconPrimary, ), - ), - ); + onPressed: _isOpen ? close : open, + ); + } else { + return SizedBox( + key: _key, + height: 56, + width: 56, + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonStyle(context) + ?.copyWith( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context) + .extension<StackColors>()! + .buttonBackBorderSecondary, + width: 1, + ), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + onPressed: _isOpen ? close : open, + child: SvgPicture.asset( + Assets.svg.list, + width: 20, + height: 20, + ), + ), + ); + } } }