From 33d52bb42453247c2ec6afc41f9d0fc06800bb0d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 24 Aug 2026 19:11:27 -0500 Subject: [PATCH 1/2] style: format coin control files with the dart formatter These three files predate the current formatter, so any change to them fails the repository's CI format gate. Reformat them on their own so the change that follows reads as a behaviour diff. --- lib/pages/coin_control/coin_control_view.dart | 771 +++++++++--------- lib/pages/coin_control/utxo_card.dart | 47 +- .../coin_control/utxo_row.dart | 84 +- 3 files changed, 428 insertions(+), 474 deletions(-) diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index 7960733643..9636190990 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -123,12 +123,11 @@ class _CoinControlViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final minConfirms = - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms; + final minConfirms = ref + .watch(pWallets) + .getWallet(widget.walletId) + .cryptoCurrency + .minConfirms; final coin = ref.watch(pWalletCoin(widget.walletId)); final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); @@ -146,12 +145,11 @@ class _CoinControlViewState extends ConsumerState { _map = null; _list = MainDB.instance.queryUTXOsSync( walletId: widget.walletId, - filter: - _isSearching - ? CCFilter.all - : _showBlocked - ? CCFilter.frozen - : CCFilter.available, + filter: _isSearching + ? CCFilter.all + : _showBlocked + ? CCFilter.frozen + : CCFilter.available, sort: _sort, searchTerm: _isSearching ? searchController.text : "", cryptoCurrency: coin, @@ -168,115 +166,107 @@ class _CoinControlViewState extends ConsumerState { }, child: Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of( + context, + ).extension()!.background, appBar: AppBar( automaticallyImplyLeading: false, - leading: - _isSearching - ? null - : widget.type == CoinControlViewType.use && - _selectedAvailable.isNotEmpty - ? AppBarIconButton( - icon: XIcon( - width: 24, - height: 24, - color: - Theme.of( - context, - ).extension()!.topNavIconPrimary, - ), - onPressed: () { - setState(() { - _selectedAvailable.clear(); - }); - }, - ) - : AppBarBackButton( - onPressed: () { - unawaited(_refreshBalance()); - Navigator.of(context).pop( - widget.type == CoinControlViewType.use - ? _selectedAvailable - : null, - ); - }, - ), - title: - _isSearching - ? AppBarSearchField( - controller: searchController, - focusNode: searchFocus, - ) - : Text( - "Coin control", - style: STextStyles.navBarTitle(context), + leading: _isSearching + ? null + : widget.type == CoinControlViewType.use && + _selectedAvailable.isNotEmpty + ? AppBarIconButton( + icon: XIcon( + width: 24, + height: 24, + color: Theme.of( + context, + ).extension()!.topNavIconPrimary, ), + onPressed: () { + setState(() { + _selectedAvailable.clear(); + }); + }, + ) + : AppBarBackButton( + onPressed: () { + unawaited(_refreshBalance()); + Navigator.of(context).pop( + widget.type == CoinControlViewType.use + ? _selectedAvailable + : null, + ); + }, + ), + title: _isSearching + ? AppBarSearchField( + controller: searchController, + focusNode: searchFocus, + ) + : Text("Coin control", style: STextStyles.navBarTitle(context)), titleSpacing: 0, - actions: - _isSearching - ? [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - size: 36, - icon: SvgPicture.asset( - Assets.svg.x, - width: 20, - height: 20, - color: - Theme.of( - context, - ).extension()!.topNavIconPrimary, - ), - onPressed: () { - // show search - setState(() { - _isSearching = false; - }); - }, + actions: _isSearching + ? [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.x, + width: 20, + height: 20, + color: Theme.of( + context, + ).extension()!.topNavIconPrimary, ), + onPressed: () { + // show search + setState(() { + _isSearching = false; + }); + }, ), - ] - : [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - size: 36, - icon: SvgPicture.asset( - Assets.svg.search, - width: 20, - height: 20, - color: - Theme.of( - context, - ).extension()!.topNavIconPrimary, - ), - onPressed: () { - // show search - setState(() { - _isSearching = true; - }); - }, + ), + ] + : [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + color: Theme.of( + context, + ).extension()!.topNavIconPrimary, ), + onPressed: () { + // show search + setState(() { + _isSearching = true; + }); + }, ), - AspectRatio( - aspectRatio: 1, - child: JDropdownIconButton( - mobileAppBar: true, - groupValue: _sort, - items: CCSortDescriptor.values.toSet(), - onSelectionChanged: (CCSortDescriptor? newValue) { - if (newValue != null && newValue != _sort) { - setState(() { - _sort = newValue; - }); - } - }, - displayPrefix: "Sort by", - ), + ), + AspectRatio( + aspectRatio: 1, + child: JDropdownIconButton( + mobileAppBar: true, + groupValue: _sort, + items: CCSortDescriptor.values.toSet(), + onSelectionChanged: (CCSortDescriptor? newValue) { + if (newValue != null && newValue != _sort) { + setState(() { + _sort = newValue; + }); + } + }, + displayPrefix: "Sort by", ), - ], + ), + ], ), body: SafeArea( child: Column( @@ -294,10 +284,9 @@ class _CoinControlViewState extends ConsumerState { "outputs at your discretion. Tap the output circle to " "select.", style: STextStyles.w500_14(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ), @@ -307,15 +296,13 @@ class _CoinControlViewState extends ConsumerState { height: 48, child: Toggle( key: UniqueKey(), - onColor: - Theme.of( - context, - ).extension()!.popupBG, + onColor: Theme.of( + context, + ).extension()!.popupBG, onText: "Available outputs", - offColor: - Theme.of(context) - .extension()! - .textFieldDefaultBG, + offColor: Theme.of( + context, + ).extension()!.textFieldDefaultBG, offText: "Frozen outputs", isOn: _showBlocked, onValueChanged: (value) { @@ -336,14 +323,13 @@ class _CoinControlViewState extends ConsumerState { Expanded( child: ListView.separated( itemCount: _list!.length, - separatorBuilder: - (context, _) => const SizedBox(height: 10), + separatorBuilder: (context, _) => + const SizedBox(height: 10), itemBuilder: (context, index) { - final utxo = - MainDB.instance.isar.utxos - .where() - .idEqualTo(_list![index]) - .findFirstSync()!; + final utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; final isSelected = _selectedBlocked.contains(utxo) || @@ -385,15 +371,14 @@ class _CoinControlViewState extends ConsumerState { setState(() {}); }, onPressed: () async { - final result = await Navigator.of( - context, - ).pushNamed( - UtxoDetailsView.routeName, - arguments: Tuple2( - utxo.id, - widget.walletId, - ), - ); + final result = await Navigator.of(context) + .pushNamed( + UtxoDetailsView.routeName, + arguments: Tuple2( + utxo.id, + widget.walletId, + ), + ); if (mounted && result == "refresh") { setState(() {}); } @@ -405,244 +390,236 @@ class _CoinControlViewState extends ConsumerState { if (!_isSearching) _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()!; + 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 && - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, + return UtxoCard( + key: Key( + "${utxo.walletId}_${utxo.id}_$isSelected", + ), + walletId: widget.walletId, + utxo: utxo, + canSelect: + widget.type == + CoinControlViewType.manage || + (widget.type == + CoinControlViewType.use && + !_showBlocked && + _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, + ), ), ), - ), - )), - 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") { + )), + 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(); + 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()! - .backgroundAppBar, - background: - Theme.of( - context, - ).extension()!.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, + return Expandable2( + border: Theme.of(context) + .extension()! + .backgroundAppBar, + background: Theme.of( + context, + ).extension()!.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, + 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, + RotateIcon( + animationDurationMultiplier: + 0.2 * entry.value.length, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + curve: Curves.easeInOut, + controller: _controller, ), - curve: Curves.easeInOut, - controller: _controller, - ), - ], + ], + ), ), - ), - children: - entry.value.map((id) { - final utxo = - MainDB.instance.isar.utxos - .where() - .idEqualTo(id) - .findFirstSync()!; + children: entry.value.map((id) { + final utxo = MainDB + .instance + .isar + .utxos + .where() + .idEqualTo(id) + .findFirstSync()!; - final isSelected = - _selectedBlocked.contains( - utxo, - ) || - _selectedAvailable.contains( - utxo, - ); + 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 && - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, - ), + 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 && + _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, ), ), - )), - initialSelectedState: isSelected, - onSelectedChanged: (value) { - if (value) { - utxo.isBlocked - ? _selectedBlocked.add( - utxo, - ) - : _selectedAvailable.add( + ), + )), + initialSelectedState: isSelected, + onSelectedChanged: (value) { + if (value) { + utxo.isBlocked + ? _selectedBlocked.add(utxo) + : _selectedAvailable.add( utxo, ); - } else { - utxo.isBlocked - ? _selectedBlocked.remove( + } else { + utxo.isBlocked + ? _selectedBlocked.remove( utxo, ) - : _selectedAvailable - .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(() {}); - }, - onPressed: () async { - final result = - await Navigator.of( - context, - ).pushNamed( - UtxoDetailsView.routeName, - arguments: Tuple2( - utxo.id, - widget.walletId, - ), - ); - if (mounted && - result == "refresh") { - setState(() {}); - } - }, - ); - }).toList(), - ); - }, + } + }, + ); + }).toList(), + ); + }, + ), ), - ), ], ), ), @@ -652,10 +629,9 @@ class _CoinControlViewState extends ConsumerState { widget.type == CoinControlViewType.manage) Container( decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.backgroundAppBar, + color: Theme.of( + context, + ).extension()!.backgroundAppBar, boxShadow: [ Theme.of( context, @@ -690,10 +666,9 @@ class _CoinControlViewState extends ConsumerState { if (!_showBlocked && widget.type == CoinControlViewType.use) Container( decoration: BoxDecoration( - color: - Theme.of( - context, - ).extension()!.backgroundAppBar, + color: Theme.of( + context, + ).extension()!.backgroundAppBar, boxShadow: [ Theme.of( context, @@ -722,13 +697,13 @@ class _CoinControlViewState extends ConsumerState { builder: (context) { final int selectedSumInt = _selectedAvailable.isEmpty - ? 0 - : _selectedAvailable - .map((e) => e.value) - .reduce( - (value, element) => - value += element, - ); + ? 0 + : _selectedAvailable + .map((e) => e.value) + .reduce( + (value, element) => + value += element, + ); final selectedSum = selectedSumInt .toAmountAsRaw( fractionDigits: @@ -738,33 +713,26 @@ class _CoinControlViewState extends ConsumerState { ref .watch(pAmountFormatter(coin)) .format(selectedSum), - style: - widget.requestedTotal == null - ? STextStyles.w600_14( - context, - ) - : STextStyles.w600_14( - context, - ).copyWith( - color: - selectedSum >= - widget - .requestedTotal! - ? Theme.of( - context, - ) - .extension< - StackColors - >()! - .accentColorGreen - : Theme.of( - context, - ) - .extension< - StackColors - >()! - .accentColorRed, - ), + style: widget.requestedTotal == null + ? STextStyles.w600_14(context) + : STextStyles.w600_14( + context, + ).copyWith( + color: + selectedSum >= + widget + .requestedTotal! + ? Theme.of(context) + .extension< + StackColors + >()! + .accentColorGreen + : Theme.of(context) + .extension< + StackColors + >()! + .accentColorRed, + ), ); }, ), @@ -775,10 +743,9 @@ class _CoinControlViewState extends ConsumerState { Container( width: double.infinity, height: 1.5, - color: - Theme.of(context) - .extension()! - .backgroundAppBar, + color: Theme.of(context) + .extension()! + .backgroundAppBar, ), if (widget.requestedTotal != null) Padding( diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index 624b41eee9..a576928dc9 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -94,8 +94,9 @@ class _UtxoCardState extends ConsumerState { focusElevation: 0, highlightElevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), onPressed: widget.onPressed, child: child, @@ -124,45 +125,43 @@ class _UtxoCardState extends ConsumerState { ), child: UTXOStatusIcon( blocked: utxo.isBlocked, - status: _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, + status: + _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet(widget.walletId), + ), ), - ), - ), - ) + ) ? UTXOStatusIconStatus.confirmed : UTXOStatusIconStatus.unconfirmed, - background: - Theme.of(context).extension()!.popupBG, + background: Theme.of( + context, + ).extension()!.popupBG, selected: _selected, width: 32, height: 32, ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( utxo.value.toAmountAsRaw( fractionDigits: coin.fractionDigits, ), ), style: STextStyles.w600_14(context), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Row( children: [ Flexible( @@ -171,9 +170,9 @@ class _UtxoCardState extends ConsumerState { ? utxo.name : utxo.address ?? utxo.txid, style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ), diff --git a/lib/pages_desktop_specific/coin_control/utxo_row.dart b/lib/pages_desktop_specific/coin_control/utxo_row.dart index e27d970fc5..0aa2986bb5 100644 --- a/lib/pages_desktop_specific/coin_control/utxo_row.dart +++ b/lib/pages_desktop_specific/coin_control/utxo_row.dart @@ -78,19 +78,17 @@ class _UtxoRowState extends ConsumerState { void _details() async { await showDialog( context: context, - builder: - (context) => - UtxoDetailsView(utxoId: utxo.id, walletId: widget.walletId), + builder: (context) => + UtxoDetailsView(utxoId: utxo.id, walletId: widget.walletId), ); } @override void initState() { - utxo = - MainDB.instance.isar.utxos - .where() - .idEqualTo(widget.data.utxoId) - .findFirstSync()!; + utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(widget.data.utxoId) + .findFirstSync()!; stream = MainDB.instance.watchUTXO(id: utxo.id); super.initState(); @@ -110,21 +108,13 @@ class _UtxoRowState extends ConsumerState { } return RoundedContainer( - borderColor: - widget.compact && widget.compactWithBorder - ? Theme.of( - context, - ).extension()!.textFieldDefaultBG - : null, + borderColor: widget.compact && widget.compactWithBorder + ? Theme.of(context).extension()!.textFieldDefaultBG + : null, color: Theme.of(context).extension()!.popupBG, - boxShadow: - widget.data.selected && widget.raiseOnSelected - ? [ - Theme.of( - context, - ).extension()!.standardBoxShadow, - ] - : null, + boxShadow: widget.data.selected && widget.raiseOnSelected + ? [Theme.of(context).extension()!.standardBoxShadow] + : null, child: Row( children: [ if (!(widget.compact && utxo.isBlocked)) @@ -143,21 +133,19 @@ class _UtxoRowState extends ConsumerState { blocked: utxo.isBlocked, status: (coin is Namecoin - ? (ref.watch(pWallets).getWallet(widget.walletId) - as NamecoinWallet) - .checkUtxoConfirmed( - utxo, - ref.watch( - pWalletChainHeight(widget.walletId), - ), - ) - : utxo.isConfirmed( - ref.watch(pWalletChainHeight(widget.walletId)), - coin.minConfirms, - coin.minCoinbaseConfirms, - )) - ? UTXOStatusIconStatus.confirmed - : UTXOStatusIconStatus.unconfirmed, + ? (ref.watch(pWallets).getWallet(widget.walletId) + as NamecoinWallet) + .checkUtxoConfirmed( + utxo, + ref.watch(pWalletChainHeight(widget.walletId)), + ) + : utxo.isConfirmed( + ref.watch(pWalletChainHeight(widget.walletId)), + coin.minConfirms, + coin.minCoinbaseConfirms, + )) + ? UTXOStatusIconStatus.confirmed + : UTXOStatusIconStatus.unconfirmed, background: Theme.of(context).extension()!.popupBG, selected: false, width: 32, @@ -207,13 +195,13 @@ class _UtxoRowState extends ConsumerState { utxo.name.isNotEmpty ? utxo.name : utxo.address ?? utxo.txid, - textAlign: - widget.compact ? TextAlign.left : TextAlign.center, + textAlign: widget.compact + ? TextAlign.left + : TextAlign.center, style: STextStyles.w500_12(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ), @@ -222,11 +210,11 @@ class _UtxoRowState extends ConsumerState { widget.compact ? CustomTextButton(text: "Details", onTap: _details) : SecondaryButton( - width: 120, - buttonHeight: ButtonHeight.xs, - label: "Details", - onPressed: _details, - ), + width: 120, + buttonHeight: ButtonHeight.xs, + label: "Details", + onPressed: _details, + ), ], ), ); From 56f1a6747927e1f6fb0da5b4dc81ac5a8f5ce27a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Mon, 24 Aug 2026 19:11:33 -0500 Subject: [PATCH 2/2] feat(coin control): toggle output selection by tapping a row Coin control rows behaved like "open details" buttons: tapping anywhere on a card navigated away, and selection was only reachable through the small status circle. Tapping a row now toggles its selection on both mobile and desktop, and a trailing options button takes over navigation to the output details. SelectableUtxoSurface is the shared tap target; coin_control_rules.dart holds the filter and selection predicates the mobile view uses across its flat, search and grouped-by-address lists. The grouped query now honours the Available/Frozen filter instead of always querying every output, so a frozen output selected in grouped mode can be unfrozen from there. --- .../coin_control/coin_control_rules.dart | 24 ++++ lib/pages/coin_control/coin_control_view.dart | 129 +++++++++--------- .../coin_control/selectable_utxo_surface.dart | 57 ++++++++ lib/pages/coin_control/utxo_card.dart | 127 +++++++++-------- .../coin_control/utxo_row.dart | 64 +++++++-- .../coin_control/coin_control_rules_test.dart | 55 ++++++++ .../selectable_utxo_surface_test.dart | 116 ++++++++++++++++ 7 files changed, 433 insertions(+), 139 deletions(-) create mode 100644 lib/pages/coin_control/coin_control_rules.dart create mode 100644 lib/pages/coin_control/selectable_utxo_surface.dart create mode 100644 test/pages/coin_control/coin_control_rules_test.dart create mode 100644 test/widget_tests/selectable_utxo_surface_test.dart diff --git a/lib/pages/coin_control/coin_control_rules.dart b/lib/pages/coin_control/coin_control_rules.dart new file mode 100644 index 0000000000..97b0471a92 --- /dev/null +++ b/lib/pages/coin_control/coin_control_rules.dart @@ -0,0 +1,24 @@ +/* + * 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. + */ + +import '../../db/isar/main_db.dart'; + +CCFilter coinControlFilter({ + required bool isSearching, + required bool showBlocked, +}) => isSearching + ? CCFilter.all + : showBlocked + ? CCFilter.frozen + : CCFilter.available; + +bool canSelectCoinControlOutput({ + required bool isManageMode, + required bool isBlocked, + required bool isConfirmed, +}) => isManageMode || (!isBlocked && isConfirmed); diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index 9636190990..6a4bb4c2e0 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -41,6 +41,7 @@ import '../../widgets/icon_widgets/x_icon.dart'; import '../../widgets/rounded_container.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/toggle.dart'; +import 'coin_control_rules.dart'; import 'utxo_card.dart'; import 'utxo_details_view.dart'; @@ -123,20 +124,19 @@ class _CoinControlViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final minConfirms = ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms; - final coin = ref.watch(pWalletCoin(widget.walletId)); final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); + final filter = coinControlFilter( + isSearching: _isSearching, + showBlocked: _showBlocked, + ); + if (_sort == CCSortDescriptor.address && !_isSearching) { _list = null; _map = MainDB.instance.queryUTXOsGroupedByAddressSync( walletId: widget.walletId, - filter: CCFilter.all, + filter: filter, sort: _sort, searchTerm: "", cryptoCurrency: coin, @@ -145,11 +145,7 @@ class _CoinControlViewState extends ConsumerState { _map = null; _list = MainDB.instance.queryUTXOsSync( walletId: widget.walletId, - filter: _isSearching - ? CCFilter.all - : _showBlocked - ? CCFilter.frozen - : CCFilter.available, + filter: filter, sort: _sort, searchTerm: _isSearching ? searchController.text : "", cryptoCurrency: coin, @@ -281,8 +277,8 @@ class _CoinControlViewState extends ConsumerState { RoundedWhiteContainer( child: Text( "This option allows you to control, freeze, and utilize " - "outputs at your discretion. Tap the output circle to " - "select.", + "outputs at your discretion. Tap an output to select it, " + "or use the options button for more actions.", style: STextStyles.w500_14(context).copyWith( color: Theme.of( context, @@ -291,7 +287,7 @@ class _CoinControlViewState extends ConsumerState { ), ), if (!_isSearching) const SizedBox(height: 10), - if (!(_isSearching || _map != null)) + if (!_isSearching) SizedBox( height: 48, child: Toggle( @@ -341,22 +337,21 @@ class _CoinControlViewState extends ConsumerState { ), walletId: widget.walletId, utxo: utxo, - canSelect: - widget.type == - CoinControlViewType.manage || - (widget.type == CoinControlViewType.use && - !utxo.isBlocked && - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, - ), - ), - ), - )), + canSelect: canSelectCoinControlOutput( + isManageMode: + widget.type == + CoinControlViewType.manage, + isBlocked: utxo.isBlocked, + isConfirmed: _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet(widget.walletId), + ), + ), + ), + ), initialSelectedState: isSelected, onSelectedChanged: (value) { if (value) { @@ -370,7 +365,7 @@ class _CoinControlViewState extends ConsumerState { } setState(() {}); }, - onPressed: () async { + onOptionsPressed: () async { final result = await Navigator.of(context) .pushNamed( UtxoDetailsView.routeName, @@ -410,23 +405,23 @@ class _CoinControlViewState extends ConsumerState { ), walletId: widget.walletId, utxo: utxo, - canSelect: - widget.type == - CoinControlViewType.manage || - (widget.type == - CoinControlViewType.use && - !_showBlocked && - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, - ), - ), - ), - )), + canSelect: canSelectCoinControlOutput( + isManageMode: + widget.type == + CoinControlViewType.manage, + isBlocked: utxo.isBlocked, + isConfirmed: _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, + ), + ), + ), + ), + ), initialSelectedState: isSelected, onSelectedChanged: (value) { if (value) { @@ -442,7 +437,7 @@ class _CoinControlViewState extends ConsumerState { } setState(() {}); }, - onPressed: () async { + onOptionsPressed: () async { final result = await Navigator.of( context, @@ -561,24 +556,24 @@ class _CoinControlViewState extends ConsumerState { walletId: widget.walletId, utxo: utxo, canSelect: - widget.type == - CoinControlViewType - .manage || - (widget.type == - CoinControlViewType - .use && - !utxo.isBlocked && - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, - ), + canSelectCoinControlOutput( + isManageMode: + widget.type == + CoinControlViewType + .manage, + isBlocked: utxo.isBlocked, + isConfirmed: _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, ), ), - )), + ), + ), + ), initialSelectedState: isSelected, onSelectedChanged: (value) { if (value) { @@ -598,7 +593,7 @@ class _CoinControlViewState extends ConsumerState { } setState(() {}); }, - onPressed: () async { + onOptionsPressed: () async { final result = await Navigator.of( context, diff --git a/lib/pages/coin_control/selectable_utxo_surface.dart b/lib/pages/coin_control/selectable_utxo_surface.dart new file mode 100644 index 0000000000..3c9505b34f --- /dev/null +++ b/lib/pages/coin_control/selectable_utxo_surface.dart @@ -0,0 +1,57 @@ +/* + * 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. + */ + +import 'package:flutter/material.dart'; + +import '../../utilities/constants.dart'; + +class SelectableUtxoSurface extends StatelessWidget { + const SelectableUtxoSurface({ + super.key, + required this.canSelect, + required this.selected, + required this.onToggle, + required this.child, + this.color, + }); + + final bool canSelect; + final bool selected; + final VoidCallback onToggle; + final Widget child; + final Color? color; + + @override + Widget build(BuildContext context) { + if (!canSelect) { + return child; + } + + return Semantics( + selected: selected, + child: MaterialButton( + minWidth: 0, + padding: EdgeInsets.zero, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + color: color, + elevation: 0, + disabledElevation: 0, + hoverElevation: 0, + focusElevation: 0, + highlightElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: onToggle, + child: child, + ), + ); + } +} diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index a576928dc9..353ea44244 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import '../../db/isar/main_db.dart'; import '../../models/isar/models/isar_models.dart'; @@ -17,14 +18,15 @@ import '../../providers/global/wallets_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; -import '../../utilities/constants.dart'; +import '../../utilities/assets.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../wallets/wallet/wallet.dart'; -import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/icon_widgets/utxo_status_icon.dart'; import '../../widgets/rounded_container.dart'; +import 'selectable_utxo_surface.dart'; class UtxoCard extends ConsumerStatefulWidget { const UtxoCard({ @@ -34,14 +36,14 @@ class UtxoCard extends ConsumerStatefulWidget { required this.onSelectedChanged, required this.initialSelectedState, required this.canSelect, - this.onPressed, + this.onOptionsPressed, }); final String walletId; final UTXO utxo; final void Function(bool) onSelectedChanged; final bool initialSelectedState; - final VoidCallback? onPressed; + final VoidCallback? onOptionsPressed; final bool canSelect; @override @@ -75,6 +77,25 @@ class _UtxoCardState extends ConsumerState { super.initState(); } + void _setSelected(bool selected) { + if (_selected == selected) { + return; + } + _selected = selected; + widget.onSelectedChanged(_selected); + setState(() {}); + } + + void _toggleSelected() => _setSelected(!_selected); + + @override + void didUpdateWidget(covariant UtxoCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.initialSelectedState != widget.initialSelectedState) { + _selected = widget.initialSelectedState; + } + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -82,29 +103,13 @@ class _UtxoCardState extends ConsumerState { final coin = ref.watch(pWalletCoin(widget.walletId)); final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); - return ConditionalParent( - condition: widget.onPressed != null, - builder: (child) => MaterialButton( - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - color: Theme.of(context).extension()!.popupBG, - elevation: 0, - disabledElevation: 0, - hoverElevation: 0, - focusElevation: 0, - highlightElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: widget.onPressed, - child: child, - ), + return SelectableUtxoSurface( + canSelect: widget.canSelect, + selected: _selected, + onToggle: _toggleSelected, + color: Theme.of(context).extension()!.popupBG, child: RoundedContainer( - color: widget.onPressed == null - ? Theme.of(context).extension()!.popupBG - : Colors.transparent, + color: Theme.of(context).extension()!.popupBG, child: StreamBuilder( stream: stream, builder: (context, snapshot) { @@ -113,37 +118,24 @@ class _UtxoCardState extends ConsumerState { } return Row( children: [ - ConditionalParent( - condition: widget.canSelect, - builder: (child) => GestureDetector( - onTap: () { - _selected = !_selected; - widget.onSelectedChanged(_selected); - setState(() {}); - }, - child: child, - ), - child: UTXOStatusIcon( - blocked: utxo.isBlocked, - status: - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet(widget.walletId), - ), - ), - ) - ? UTXOStatusIconStatus.confirmed - : UTXOStatusIconStatus.unconfirmed, - background: Theme.of( - context, - ).extension()!.popupBG, - selected: _selected, - width: 32, - height: 32, - ), + UTXOStatusIcon( + blocked: utxo.isBlocked, + status: + _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select((s) => s.getWallet(widget.walletId)), + ), + ) + ? UTXOStatusIconStatus.confirmed + : UTXOStatusIconStatus.unconfirmed, + background: Theme.of( + context, + ).extension()!.popupBG, + selected: _selected, + width: 32, + height: 32, ), const SizedBox(width: 10), Expanded( @@ -181,6 +173,27 @@ class _UtxoCardState extends ConsumerState { ], ), ), + if (widget.onOptionsPressed != null) const SizedBox(width: 10), + if (widget.onOptionsPressed != null) + AppBarIconButton( + semanticsLabel: "Output options", + tooltip: "Output options", + size: 36, + shadows: const [], + color: Theme.of(context).extension()!.popupBG, + icon: SvgPicture.asset( + Assets.svg.verticalEllipsis, + colorFilter: ColorFilter.mode( + Theme.of( + context, + ).extension()!.textSubtitle1, + BlendMode.srcIn, + ), + width: 20, + height: 20, + ), + onPressed: widget.onOptionsPressed, + ), ], ); }, diff --git a/lib/pages_desktop_specific/coin_control/utxo_row.dart b/lib/pages_desktop_specific/coin_control/utxo_row.dart index 0aa2986bb5..0c180623c8 100644 --- a/lib/pages_desktop_specific/coin_control/utxo_row.dart +++ b/lib/pages_desktop_specific/coin_control/utxo_row.dart @@ -10,22 +10,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar_community/isar.dart'; import '../../db/isar/main_db.dart'; import '../../models/isar/models/isar_models.dart'; +import '../../pages/coin_control/selectable_utxo_surface.dart'; import '../../pages/coin_control/utxo_details_view.dart'; import '../../providers/global/wallets_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/assets.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/crypto_currency/coins/namecoin.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../widgets/conditional_parent.dart'; -import '../../widgets/custom_buttons/blue_text_button.dart'; -import '../../widgets/desktop/secondary_button.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/icon_widgets/utxo_status_icon.dart'; import '../../widgets/rounded_container.dart'; @@ -83,6 +85,23 @@ class _UtxoRowState extends ConsumerState { ); } + void _toggleSelected() { + if (widget.compact && utxo.isBlocked) { + return; + } + _setSelected(!widget.data.selected); + } + + void _setSelected(bool selected) { + if (widget.data.selected == selected) { + return; + } + setState(() { + widget.data.selected = selected; + }); + widget.onSelectionChanged?.call(widget.data); + } + @override void initState() { utxo = MainDB.instance.isar.utxos @@ -107,7 +126,7 @@ class _UtxoRowState extends ConsumerState { utxo = snapshot.data!; } - return RoundedContainer( + final content = RoundedContainer( borderColor: widget.compact && widget.compactWithBorder ? Theme.of(context).extension()!.textFieldDefaultBG : null, @@ -121,10 +140,9 @@ class _UtxoRowState extends ConsumerState { Checkbox( value: widget.data.selected, onChanged: (value) { - setState(() { - widget.data.selected = value!; - }); - widget.onSelectionChanged?.call(widget.data); + if (value != null) { + _setSelected(value); + } }, ), if (!(widget.compact && utxo.isBlocked)) @@ -207,17 +225,33 @@ class _UtxoRowState extends ConsumerState { ), ), const SizedBox(width: 10), - widget.compact - ? CustomTextButton(text: "Details", onTap: _details) - : SecondaryButton( - width: 120, - buttonHeight: ButtonHeight.xs, - label: "Details", - onPressed: _details, - ), + AppBarIconButton( + semanticsLabel: "Output options", + tooltip: "Output options", + size: 36, + shadows: const [], + color: Theme.of(context).extension()!.popupBG, + icon: SvgPicture.asset( + Assets.svg.verticalEllipsis, + colorFilter: ColorFilter.mode( + Theme.of(context).extension()!.textSubtitle1, + BlendMode.srcIn, + ), + width: 20, + height: 20, + ), + onPressed: _details, + ), ], ), ); + + return SelectableUtxoSurface( + canSelect: !(widget.compact && utxo.isBlocked), + selected: widget.data.selected, + onToggle: _toggleSelected, + child: content, + ); }, ); } diff --git a/test/pages/coin_control/coin_control_rules_test.dart b/test/pages/coin_control/coin_control_rules_test.dart new file mode 100644 index 0000000000..f316b3fc4c --- /dev/null +++ b/test/pages/coin_control/coin_control_rules_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/pages/coin_control/coin_control_rules.dart'; + +void main() { + test("uses one filter contract for flat and grouped output lists", () { + expect( + coinControlFilter(isSearching: true, showBlocked: false), + CCFilter.all, + ); + expect( + coinControlFilter(isSearching: false, showBlocked: false), + CCFilter.available, + ); + expect( + coinControlFilter(isSearching: false, showBlocked: true), + CCFilter.frozen, + ); + }); + + test("use mode excludes blocked and unconfirmed outputs", () { + expect( + canSelectCoinControlOutput( + isManageMode: false, + isBlocked: false, + isConfirmed: true, + ), + isTrue, + ); + expect( + canSelectCoinControlOutput( + isManageMode: false, + isBlocked: true, + isConfirmed: true, + ), + isFalse, + ); + expect( + canSelectCoinControlOutput( + isManageMode: false, + isBlocked: false, + isConfirmed: false, + ), + isFalse, + ); + expect( + canSelectCoinControlOutput( + isManageMode: true, + isBlocked: true, + isConfirmed: false, + ), + isTrue, + ); + }); +} diff --git a/test/widget_tests/selectable_utxo_surface_test.dart b/test/widget_tests/selectable_utxo_surface_test.dart new file mode 100644 index 0000000000..7bcb70c213 --- /dev/null +++ b/test/widget_tests/selectable_utxo_surface_test.dart @@ -0,0 +1,116 @@ +import 'dart:ui' show Tristate; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/pages/coin_control/selectable_utxo_surface.dart'; + +void main() { + Widget testApp({ + required bool canSelect, + required bool selected, + required VoidCallback onToggle, + required VoidCallback onOptions, + }) => MaterialApp( + home: Scaffold( + body: SelectableUtxoSurface( + canSelect: canSelect, + selected: selected, + onToggle: onToggle, + child: SizedBox( + width: 300, + height: 64, + child: Row( + children: [ + const Expanded(child: Text("Output 1")), + IconButton( + key: const Key("options"), + onPressed: onOptions, + icon: const Icon(Icons.more_vert), + ), + ], + ), + ), + ), + ), + ); + + testWidgets("row and nested options have independent actions", ( + tester, + ) async { + var toggles = 0; + var options = 0; + + await tester.pumpWidget( + testApp( + canSelect: true, + selected: false, + onToggle: () => toggles++, + onOptions: () => options++, + ), + ); + + await tester.tap(find.text("Output 1")); + await tester.pump(); + expect(toggles, 1); + expect(options, 0); + + await tester.tap(find.byKey(const Key("options"))); + await tester.pump(); + expect(toggles, 1); + expect(options, 1); + }); + + testWidgets("row selection supports keyboard activation and semantics", ( + tester, + ) async { + final semantics = tester.ensureSemantics(); + var toggles = 0; + + await tester.pumpWidget( + testApp( + canSelect: true, + selected: true, + onToggle: () => toggles++, + onOptions: () {}, + ), + ); + + final selectedSurface = find.byWidgetPredicate( + (widget) => widget is Semantics && widget.properties.selected == true, + ); + expect( + tester.getSemantics(selectedSurface).flagsCollection.isSelected, + Tristate.isTrue, + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pump(); + expect(toggles, 1); + + semantics.dispose(); + }); + + testWidgets("nonselectable rows retain their options action", (tester) async { + var toggles = 0; + var options = 0; + + await tester.pumpWidget( + testApp( + canSelect: false, + selected: false, + onToggle: () => toggles++, + onOptions: () => options++, + ), + ); + + await tester.tap(find.text("Output 1")); + await tester.tap(find.byKey(const Key("options"))); + await tester.pump(); + + expect(toggles, 0); + expect(options, 1); + }); +}