desktop coin control

This commit is contained in:
julian 2023-03-16 12:10:59 -06:00
parent 6c1d9d2912
commit 6f7f9c24eb
8 changed files with 990 additions and 238 deletions

View file

@ -98,4 +98,85 @@ extension MainDBQueries on MainDB {
} }
return ids; return ids;
} }
Map<String, List<Id>> queryUTXOsGroupedByAddressSync({
required String walletId,
required CCFilter filter,
required CCSortDescriptor sort,
required String searchTerm,
required Coin coin,
}) {
var preSort = getUTXOs(walletId).filter().group((q) {
final qq = q.group(
(q) => q.usedIsNull().or().usedEqualTo(false),
);
switch (filter) {
case CCFilter.frozen:
return qq.and().isBlockedEqualTo(true);
case CCFilter.available:
return qq.and().isBlockedEqualTo(false);
case CCFilter.all:
return qq;
}
});
if (searchTerm.isNotEmpty) {
preSort = preSort.and().group(
(q) {
var qq = q.addressContains(searchTerm, caseSensitive: false);
qq = qq.or().nameContains(searchTerm, caseSensitive: false);
qq = qq.or().group(
(q) => q
.isBlockedEqualTo(true)
.and()
.blockedReasonContains(searchTerm, caseSensitive: false),
);
qq = qq.or().txidContains(searchTerm, caseSensitive: false);
qq = qq.or().blockHashContains(searchTerm, caseSensitive: false);
final maybeDecimal = Decimal.tryParse(searchTerm);
if (maybeDecimal != null) {
qq = qq.or().valueEqualTo(
Format.decimalAmountToSatoshis(
maybeDecimal,
coin,
),
);
}
final maybeInt = int.tryParse(searchTerm);
if (maybeInt != null) {
qq = qq.or().valueEqualTo(maybeInt);
}
return qq;
},
);
}
final List<UTXO> utxos;
switch (sort) {
case CCSortDescriptor.age:
utxos = preSort.sortByBlockHeight().findAllSync();
break;
case CCSortDescriptor.address:
utxos = preSort.sortByAddress().findAllSync();
break;
case CCSortDescriptor.value:
utxos = preSort.sortByValueDesc().findAllSync();
break;
}
final Map<String, List<Id>> results = {};
for (final utxo in utxos) {
if (results[utxo.address!] == null) {
results[utxo.address!] = [];
}
results[utxo.address!]!.add(utxo.id);
}
return results;
}
} }

View file

@ -9,6 +9,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart';
import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
@ -124,6 +125,7 @@ class _ConfirmTransactionViewState
]); ]);
txid = results.first as String; txid = results.first as String;
ref.refresh(desktopUseUTXOs);
// save note // save note
await ref await ref

View file

@ -1,3 +1,4 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
@ -9,27 +10,34 @@ import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_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/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/widgets/toggle.dart'; import 'package:stackwallet/widgets/toggle.dart';
final desktopUseUTXOs = StateProvider.autoDispose((ref) => <UTXO>{}); final desktopUseUTXOs = StateProvider((ref) => <UTXO>{});
class DesktopCoinControlUseDialog extends ConsumerStatefulWidget { class DesktopCoinControlUseDialog extends ConsumerStatefulWidget {
const DesktopCoinControlUseDialog({ const DesktopCoinControlUseDialog({
Key? key, Key? key,
required this.walletId, required this.walletId,
this.amountToSend,
}) : super(key: key); }) : super(key: key);
final String walletId; final String walletId;
final Decimal? amountToSend;
@override @override
ConsumerState<DesktopCoinControlUseDialog> createState() => ConsumerState<DesktopCoinControlUseDialog> createState() =>
@ -42,13 +50,22 @@ class _DesktopCoinControlUseDialogState
late final Coin coin; late final Coin coin;
final searchFieldFocusNode = FocusNode(); final searchFieldFocusNode = FocusNode();
final Set<UtxoRowData> _selectedUTXOs = {}; final Set<UtxoRowData> _selectedUTXOsData = {};
final Set<UTXO> _selectedUTXOs = {};
Map<String, List<Id>>? _map;
List<Id>? _list;
String _searchString = ""; String _searchString = "";
CCFilter _filter = CCFilter.available; CCFilter _filter = CCFilter.available;
CCSortDescriptor _sort = CCSortDescriptor.age; CCSortDescriptor _sort = CCSortDescriptor.age;
bool selectedChanged(Set<UTXO> newSelected) {
if (ref.read(desktopUseUTXOs).length != newSelected.length) return true;
return !ref.read(desktopUseUTXOs).containsAll(newSelected);
}
@override @override
void initState() { void initState() {
_searchController = TextEditingController(); _searchController = TextEditingController();
@ -56,6 +73,13 @@ class _DesktopCoinControlUseDialogState
.read(walletsChangeNotifierProvider) .read(walletsChangeNotifierProvider)
.getManager(widget.walletId) .getManager(widget.walletId)
.coin; .coin;
for (final utxo in ref.read(desktopUseUTXOs)) {
final data = UtxoRowData(utxo.id, true);
_selectedUTXOs.add(utxo);
_selectedUTXOsData.add(data);
}
super.initState(); super.initState();
} }
@ -68,14 +92,40 @@ class _DesktopCoinControlUseDialogState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ids = MainDB.instance.queryUTXOsSync( debugPrint("BUILD: $runtimeType");
walletId: widget.walletId,
filter: _filter, if (_sort == CCSortDescriptor.address) {
sort: _sort, _list = null;
searchTerm: _searchString, _map = MainDB.instance.queryUTXOsGroupedByAddressSync(
walletId: widget.walletId,
filter: _filter,
sort: _sort,
searchTerm: _searchString,
coin: coin,
);
} else {
_map = null;
_list = MainDB.instance.queryUTXOsSync(
walletId: widget.walletId,
filter: _filter,
sort: _sort,
searchTerm: _searchString,
coin: coin,
);
}
final selectedSum = Format.satoshisToAmount(
_selectedUTXOs
.map((e) => e.value)
.fold(0, (value, element) => value += element),
coin: coin, coin: coin,
); );
final enableApply = widget.amountToSend == null
? selectedChanged(_selectedUTXOs)
: selectedChanged(_selectedUTXOs) &&
widget.amountToSend! <= selectedSum;
return DesktopDialog( return DesktopDialog(
maxWidth: 700, maxWidth: 700,
maxHeight: MediaQuery.of(context).size.height - 128, maxHeight: MediaQuery.of(context).size.height - 128,
@ -220,75 +270,165 @@ class _DesktopCoinControlUseDialogState
const SizedBox( const SizedBox(
width: 16, width: 16,
), ),
SizedBox( JDropdownIconButton(
height: 56, redrawOnScreenSizeChanged: true,
width: 56, groupValue: _sort,
child: TextButton( items: CCSortDescriptor.values.toSet(),
style: Theme.of(context) onSelectionChanged: (CCSortDescriptor? newValue) {
.extension<StackColors>()! if (newValue != null && newValue != _sort) {
.getSecondaryEnabledButtonStyle(context) setState(() {
?.copyWith( _sort = newValue;
shape: MaterialStateProperty.all( });
RoundedRectangleBorder( }
side: BorderSide( },
color: Theme.of(context) )
.extension<StackColors>()!
.buttonBackBorderSecondary,
width: 1,
),
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
),
onPressed: () {},
child: SvgPicture.asset(
Assets.svg.list,
width: 20,
height: 20,
),
),
),
], ],
), ),
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
Expanded( Expanded(
child: ListView.separated( child: _list != null
shrinkWrap: true, ? ListView.separated(
primary: false, shrinkWrap: true,
itemCount: ids.length, primary: false,
separatorBuilder: (context, _) => const SizedBox( itemCount: _list!.length,
height: 10, separatorBuilder: (context, _) => const SizedBox(
), height: 10,
itemBuilder: (context, index) { ),
final utxo = MainDB.instance.isar.utxos itemBuilder: (context, index) {
.where() final utxo = MainDB.instance.isar.utxos
.idEqualTo(ids[index]) .where()
.findFirstSync()!; .idEqualTo(_list![index])
final data = UtxoRowData(utxo.id, false); .findFirstSync()!;
data.selected = _selectedUTXOs.contains(data); final data = UtxoRowData(utxo.id, false);
data.selected = _selectedUTXOsData.contains(data);
return UtxoRow( return UtxoRow(
key: Key( key: Key(
"${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"), "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"),
data: data, data: data,
compact: true, compact: true,
walletId: widget.walletId, walletId: widget.walletId,
onSelectionChanged: (value) { onSelectionChanged: (value) {
setState(() { setState(() {
if (data.selected) { if (data.selected) {
_selectedUTXOs.add(value); _selectedUTXOsData.add(value);
} else { _selectedUTXOs.add(utxo);
_selectedUTXOs.remove(value); } else {
} _selectedUTXOsData.remove(value);
}); _selectedUTXOs.remove(utxo);
}, }
); });
}, },
), );
},
)
: 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(20),
color: Colors.transparent,
child: Row(
children: [
SvgPicture.asset(
Assets.svg.iconFor(coin: coin),
width: 24,
height: 24,
),
const SizedBox(
width: 12,
),
Expanded(
flex: 3,
child: Text(
entry.key,
style: STextStyles.w600_14(context),
),
),
Expanded(
child: Text(
"${entry.value.length} "
"output${entry.value.length > 1 ? "s" : ""}",
style: STextStyles
.desktopTextExtraExtraSmall(
context),
),
),
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 data = UtxoRowData(utxo.id, false);
data.selected =
_selectedUTXOsData.contains(data);
return UtxoRow(
key: Key(
"${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"),
data: data,
compact: true,
compactWithBorder: false,
raiseOnSelected: false,
walletId: widget.walletId,
onSelectionChanged: (value) {
setState(() {
if (data.selected) {
_selectedUTXOsData.add(value);
_selectedUTXOs.add(utxo);
} else {
_selectedUTXOsData.remove(value);
_selectedUTXOs.remove(utxo);
}
});
},
);
},
).toList(),
);
},
),
), ),
const SizedBox( const SizedBox(
height: 16, height: 16,
@ -297,29 +437,95 @@ class _DesktopCoinControlUseDialogState
color: Theme.of(context) color: Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
.textFieldDefaultBG, .textFieldDefaultBG,
padding: const EdgeInsets.all(16), padding: EdgeInsets.zero,
child: Row( child: ConditionalParent(
mainAxisAlignment: MainAxisAlignment.spaceBetween, condition: widget.amountToSend != null,
children: [ builder: (child) {
Text( return Column(
"Selected amount", mainAxisSize: MainAxisSize.min,
style: STextStyles.desktopTextExtraExtraSmall(context) children: [
.copyWith( child,
color: Theme.of(context) Container(
.extension<StackColors>()! height: 1.2,
.textDark, color: Theme.of(context)
), .extension<StackColors>()!
.popupBG,
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Amount to send",
style:
STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
Text(
"${widget.amountToSend!.toStringAsFixed(
coin.decimals,
)}"
" ${coin.ticker}",
style:
STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
],
),
),
],
);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Selected amount",
style: STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
Text(
"${selectedSum.toStringAsFixed(
coin.decimals,
)} ${coin.ticker}",
style: STextStyles.desktopTextExtraExtraSmall(
context)
.copyWith(
color: widget.amountToSend == null
? Theme.of(context)
.extension<StackColors>()!
.textDark
: selectedSum < widget.amountToSend!
? Theme.of(context)
.extension<StackColors>()!
.accentColorRed
: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
),
),
],
), ),
Text( ),
"LOL",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
],
), ),
), ),
const SizedBox( const SizedBox(
@ -329,14 +535,17 @@ class _DesktopCoinControlUseDialogState
children: [ children: [
Expanded( Expanded(
child: SecondaryButton( child: SecondaryButton(
enabled: _selectedUTXOs.isNotEmpty, enabled: _selectedUTXOsData.isNotEmpty,
buttonHeight: ButtonHeight.l, buttonHeight: ButtonHeight.l,
label: _selectedUTXOs.isEmpty label: _selectedUTXOsData.isEmpty
? "Clear selection" ? "Clear selection"
: "Clear selection (${_selectedUTXOs.length})", : "Clear selection (${_selectedUTXOsData.length})",
onPressed: () => setState(() { onPressed: () {
_selectedUTXOs.clear(); setState(() {
}), _selectedUTXOsData.clear();
_selectedUTXOs.clear();
});
},
), ),
), ),
const SizedBox( const SizedBox(
@ -344,9 +553,15 @@ class _DesktopCoinControlUseDialogState
), ),
Expanded( Expanded(
child: PrimaryButton( child: PrimaryButton(
enabled: _selectedUTXOs.isNotEmpty, enabled: enableApply,
buttonHeight: ButtonHeight.l, buttonHeight: ButtonHeight.l,
label: "Use coins", label: "Apply",
onPressed: () {
ref.read(desktopUseUTXOs.state).state =
_selectedUTXOs;
Navigator.of(context, rootNavigator: true).pop();
},
), ),
), ),
], ],

View file

@ -12,12 +12,15 @@ import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.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/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/custom_buttons/dropdown_button.dart'; import 'package:stackwallet/widgets/custom_buttons/dropdown_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/desktop/secondary_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/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart';
@ -44,6 +47,9 @@ class _DesktopCoinControlViewState
final Set<UtxoRowData> _selectedUTXOs = {}; final Set<UtxoRowData> _selectedUTXOs = {};
Map<String, List<Id>>? _map;
List<Id>? _list;
String _searchString = ""; String _searchString = "";
CCFilter _filter = CCFilter.all; CCFilter _filter = CCFilter.all;
@ -70,13 +76,25 @@ class _DesktopCoinControlViewState
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType"); debugPrint("BUILD: $runtimeType");
final ids = MainDB.instance.queryUTXOsSync( if (_sort == CCSortDescriptor.address) {
walletId: widget.walletId, _list = null;
filter: _filter, _map = MainDB.instance.queryUTXOsGroupedByAddressSync(
sort: _sort, walletId: widget.walletId,
searchTerm: _searchString, filter: _filter,
coin: coin, sort: _sort,
); searchTerm: _searchString,
coin: coin,
);
} else {
_map = null;
_list = MainDB.instance.queryUTXOsSync(
walletId: widget.walletId,
filter: _filter,
sort: _sort,
searchTerm: _searchString,
coin: coin,
);
}
return DesktopScaffold( return DesktopScaffold(
appBar: DesktopAppBar( appBar: DesktopAppBar(
@ -264,35 +282,135 @@ class _DesktopCoinControlViewState
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 24, horizontal: 24,
), ),
child: ListView.separated( child: _list != null
itemCount: ids.length, ? ListView.separated(
separatorBuilder: (context, _) => const SizedBox( itemCount: _list!.length,
height: 10, separatorBuilder: (context, _) => const SizedBox(
), height: 10,
itemBuilder: (context, index) { ),
final utxo = MainDB.instance.isar.utxos itemBuilder: (context, index) {
.where() final utxo = MainDB.instance.isar.utxos
.idEqualTo(ids[index]) .where()
.findFirstSync()!; .idEqualTo(_list![index])
final data = UtxoRowData(utxo.id, false); .findFirstSync()!;
data.selected = _selectedUTXOs.contains(data); final data = UtxoRowData(utxo.id, false);
data.selected = _selectedUTXOs.contains(data);
return UtxoRow( return UtxoRow(
key: Key("${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"), key: Key(
data: data, "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"),
walletId: widget.walletId, data: data,
onSelectionChanged: (value) { walletId: widget.walletId,
setState(() { onSelectionChanged: (value) {
if (data.selected) { setState(() {
_selectedUTXOs.add(value); if (data.selected) {
} else { _selectedUTXOs.add(value);
_selectedUTXOs.remove(value); } else {
} _selectedUTXOs.remove(value);
}); }
}, });
); },
}, );
), },
)
: 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(20),
color: Colors.transparent,
child: Row(
children: [
SvgPicture.asset(
Assets.svg.iconFor(coin: coin),
width: 24,
height: 24,
),
const SizedBox(
width: 12,
),
Expanded(
child: Text(
entry.key,
style: STextStyles.w600_14(context),
),
),
Expanded(
child: Text(
"${entry.value.length} "
"output${entry.value.length > 1 ? "s" : ""}",
style:
STextStyles.desktopTextExtraExtraSmall(
context),
),
),
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 data = UtxoRowData(utxo.id, false);
data.selected = _selectedUTXOs.contains(data);
return UtxoRow(
key: Key(
"${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"),
data: data,
walletId: widget.walletId,
raiseOnSelected: false,
onSelectionChanged: (value) {
setState(() {
if (data.selected) {
_selectedUTXOs.add(value);
} else {
_selectedUTXOs.remove(value);
}
});
},
);
},
).toList(),
);
},
),
), ),
), ),
], ],

View file

@ -42,12 +42,16 @@ class UtxoRow extends ConsumerStatefulWidget {
required this.walletId, required this.walletId,
this.onSelectionChanged, this.onSelectionChanged,
this.compact = false, this.compact = false,
this.compactWithBorder = true,
this.raiseOnSelected = true,
}) : super(key: key); }) : super(key: key);
final String walletId; final String walletId;
final UtxoRowData data; final UtxoRowData data;
final void Function(UtxoRowData)? onSelectionChanged; final void Function(UtxoRowData)? onSelectionChanged;
final bool compact; final bool compact;
final bool compactWithBorder;
final bool raiseOnSelected;
@override @override
ConsumerState<UtxoRow> createState() => _UtxoRowState(); ConsumerState<UtxoRow> createState() => _UtxoRowState();
@ -96,29 +100,31 @@ class _UtxoRowState extends ConsumerState<UtxoRow> {
} }
return RoundedContainer( return RoundedContainer(
borderColor: widget.compact borderColor: widget.compact && widget.compactWithBorder
? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG ? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG
: null, : null,
color: Theme.of(context).extension<StackColors>()!.popupBG, color: Theme.of(context).extension<StackColors>()!.popupBG,
boxShadow: widget.data.selected boxShadow: widget.data.selected && widget.raiseOnSelected
? [ ? [
Theme.of(context).extension<StackColors>()!.standardBoxShadow, Theme.of(context).extension<StackColors>()!.standardBoxShadow,
] ]
: null, : null,
child: Row( child: Row(
children: [ children: [
Checkbox( if (!(widget.compact && utxo.isBlocked))
value: widget.data.selected, Checkbox(
onChanged: (value) { value: widget.data.selected,
setState(() { onChanged: (value) {
widget.data.selected = value!; setState(() {
}); widget.data.selected = value!;
widget.onSelectionChanged?.call(widget.data); });
}, widget.onSelectionChanged?.call(widget.data);
), },
const SizedBox( ),
width: 10, if (!(widget.compact && utxo.isBlocked))
), const SizedBox(
width: 10,
),
UTXOStatusIcon( UTXOStatusIcon(
blocked: utxo.isBlocked, blocked: utxo.isBlocked,
status: utxo.isConfirmed( status: utxo.isConfirmed(

View file

@ -122,93 +122,101 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin); Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin);
} }
// confirm send all final coinControlEnabled =
if (amount == availableBalance) { ref.read(prefsChangeNotifierProvider).enableCoinControl;
final bool? shouldSendAll = await showDialog<bool>(
context: context, if (!(manager.hasCoinControlSupport && coinControlEnabled) ||
useSafeArea: false, (manager.hasCoinControlSupport &&
barrierDismissible: true, coinControlEnabled &&
builder: (context) { ref.read(desktopUseUTXOs).isEmpty)) {
return DesktopDialog( // confirm send all
maxWidth: 450, if (amount == availableBalance) {
maxHeight: double.infinity, final bool? shouldSendAll = await showDialog<bool>(
child: Padding( context: context,
padding: const EdgeInsets.only( useSafeArea: false,
left: 32, barrierDismissible: true,
bottom: 32, builder: (context) {
), return DesktopDialog(
child: Column( maxWidth: 450,
crossAxisAlignment: CrossAxisAlignment.start, maxHeight: double.infinity,
children: [ child: Padding(
Row( padding: const EdgeInsets.only(
mainAxisAlignment: MainAxisAlignment.spaceBetween, left: 32,
children: [ bottom: 32,
Text( ),
"Confirm send all", child: Column(
style: STextStyles.desktopH3(context), crossAxisAlignment: CrossAxisAlignment.start,
), children: [
const DesktopDialogCloseButton(), Row(
], mainAxisAlignment: MainAxisAlignment.spaceBetween,
),
const SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.only(
right: 32,
),
child: Text(
"You are about to send your entire balance. Would you like to continue?",
textAlign: TextAlign.left,
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
fontSize: 18,
),
),
),
const SizedBox(
height: 40,
),
Padding(
padding: const EdgeInsets.only(
right: 32,
),
child: Row(
children: [ children: [
Expanded( Text(
child: SecondaryButton( "Confirm send all",
buttonHeight: ButtonHeight.l, style: STextStyles.desktopH3(context),
label: "Cancel",
onPressed: () {
Navigator.of(context).pop(false);
},
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Yes",
onPressed: () {
Navigator.of(context).pop(true);
},
),
), ),
const DesktopDialogCloseButton(),
], ],
), ),
), const SizedBox(
], height: 12,
),
Padding(
padding: const EdgeInsets.only(
right: 32,
),
child: Text(
"You are about to send your entire balance. Would you like to continue?",
textAlign: TextAlign.left,
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
fontSize: 18,
),
),
),
const SizedBox(
height: 40,
),
Padding(
padding: const EdgeInsets.only(
right: 32,
),
child: Row(
children: [
Expanded(
child: SecondaryButton(
buttonHeight: ButtonHeight.l,
label: "Cancel",
onPressed: () {
Navigator.of(context).pop(false);
},
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Yes",
onPressed: () {
Navigator.of(context).pop(true);
},
),
),
],
),
),
],
),
), ),
), );
); },
}, );
);
if (shouldSendAll == null || shouldSendAll == false) { if (shouldSendAll == null || shouldSendAll == false) {
// cancel preview // cancel preview
return; return;
}
} }
} }
@ -261,7 +269,14 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
txDataFuture = wallet.preparePaymentCodeSend( txDataFuture = wallet.preparePaymentCodeSend(
paymentCode: paymentCode, paymentCode: paymentCode,
satoshiAmount: amount, satoshiAmount: amount,
args: {"feeRate": feeRate}, args: {
"feeRate": feeRate,
"UTXOs": (manager.hasCoinControlSupport &&
coinControlEnabled &&
ref.read(desktopUseUTXOs).isNotEmpty)
? ref.read(desktopUseUTXOs)
: null,
},
); );
} else if ((coin == Coin.firo || coin == Coin.firoTestNet) && } else if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
ref.read(publicPrivateBalanceStateProvider.state).state != ref.read(publicPrivateBalanceStateProvider.state).state !=
@ -269,13 +284,27 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic( txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic(
address: _address!, address: _address!,
satoshiAmount: amount, satoshiAmount: amount,
args: {"feeRate": ref.read(feeRateTypeStateProvider)}, args: {
"feeRate": ref.read(feeRateTypeStateProvider),
"UTXOs": (manager.hasCoinControlSupport &&
coinControlEnabled &&
ref.read(desktopUseUTXOs).isNotEmpty)
? ref.read(desktopUseUTXOs)
: null,
},
); );
} else { } else {
txDataFuture = manager.prepareSend( txDataFuture = manager.prepareSend(
address: _address!, address: _address!,
satoshiAmount: amount, satoshiAmount: amount,
args: {"feeRate": ref.read(feeRateTypeStateProvider)}, args: {
"feeRate": ref.read(feeRateTypeStateProvider),
"UTXOs": (manager.hasCoinControlSupport &&
coinControlEnabled &&
ref.read(desktopUseUTXOs).isNotEmpty)
? ref.read(desktopUseUTXOs)
: null,
},
); );
} }
@ -682,13 +711,11 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
} }
void _showDesktopCoinControl() async { void _showDesktopCoinControl() async {
if (_amountToSend == null) { await showDialog<void>(
//
}
final result = await showDialog(
context: context, context: context,
builder: (context) => DesktopCoinControlUseDialog( builder: (context) => DesktopCoinControlUseDialog(
walletId: widget.walletId, walletId: widget.walletId,
amountToSend: _amountToSend,
), ),
); );
} }
@ -1094,7 +1121,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
style: STextStyles.desktopTextExtraExtraSmall(context), style: STextStyles.desktopTextExtraExtraSmall(context),
), ),
CustomTextButton( CustomTextButton(
text: "Select coins", text: ref.watch(desktopUseUTXOs.state).state.isEmpty
? "Select coins"
: "Selected coins (${ref.watch(desktopUseUTXOs.state).state.length})",
onTap: _showDesktopCoinControl, onTap: _showDesktopCoinControl,
), ),
], ],

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart';
@ -123,6 +124,124 @@ class _JDropdownButtonState<T> extends State<JDropdownButton<T>> {
} }
} }
class JDropdownIconButton<T> extends StatefulWidget {
const JDropdownIconButton({
Key? key,
required this.items,
this.onSelectionChanged,
this.groupValue,
this.redrawOnScreenSizeChanged = false,
}) : super(key: key);
final void Function(T?)? onSelectionChanged;
final T? groupValue;
final Set<T> items;
/// setting this to true should be done carefully
final bool redrawOnScreenSizeChanged;
@override
State<JDropdownIconButton<T>> createState() => _JDropdownIconButtonState();
}
class _JDropdownIconButtonState<T> extends State<JDropdownIconButton<T>> {
final _key = GlobalKey();
bool _isOpen = false;
OverlayEntry? _entry;
void close() {
if (_isOpen) {
_entry?.remove();
_isOpen = false;
}
}
void open() {
final size = (_key.currentContext!.findRenderObject() as RenderBox).size;
_entry = OverlayEntry(
builder: (_) {
final position = (_key.currentContext!.findRenderObject() as RenderBox)
.localToGlobal(Offset.zero);
if (widget.redrawOnScreenSizeChanged) {
// trigger rebuild
MediaQuery.of(context).size;
}
return GestureDetector(
onTap: close,
child: _JDropdownButtonMenu<T>(
size: Size(200, size.height),
position: Offset(position.dx - 144, position.dy),
items: widget.items
.map(
(e) => _JDropdownButtonItem<T>(
value: e,
groupValue: widget.groupValue,
onSelected: (T value) {
widget.onSelectionChanged?.call(value);
close();
},
),
)
.toList(),
),
);
},
);
Overlay.of(context, rootOverlay: true).insert(_entry!);
_isOpen = true;
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.redrawOnScreenSizeChanged && _isOpen) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_entry?.markNeedsBuild();
});
}
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,
),
),
);
}
}
// ============================================================================= // =============================================================================
class _JDropdownButtonMenu<T> extends StatefulWidget { class _JDropdownButtonMenu<T> extends StatefulWidget {

View file

@ -0,0 +1,182 @@
import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
enum Expandable2State {
collapsed,
expanded,
}
class Expandable2Controller {
VoidCallback? toggle;
Expandable2State state = Expandable2State.collapsed;
}
class Expandable2 extends StatefulWidget {
const Expandable2({
Key? key,
required this.header,
required this.children,
this.background = Colors.white,
this.border = Colors.black,
this.animationController,
this.animation,
this.animationDurationMultiplier = 1.0,
this.onExpandWillChange,
this.onExpandChanged,
this.controller,
this.expandOverride,
}) : super(key: key);
final Widget header;
final List<Widget> children;
final Color background;
final Color border;
final AnimationController? animationController;
final Animation<double>? animation;
final double animationDurationMultiplier;
final void Function(Expandable2State)? onExpandWillChange;
final void Function(Expandable2State)? onExpandChanged;
final Expandable2Controller? controller;
final VoidCallback? expandOverride;
@override
State<Expandable2> createState() => _Expandable2State();
}
class _Expandable2State extends State<Expandable2>
with TickerProviderStateMixin {
final _key = GlobalKey();
late final AnimationController animationController;
late final Animation<double> animation;
late final Duration duration;
late final Expandable2Controller? controller;
Expandable2State _toggleState = Expandable2State.collapsed;
void toggle() {
if (animation.isDismissed) {
_toggleState = Expandable2State.expanded;
widget.onExpandWillChange?.call(_toggleState);
animationController
.forward()
.then((_) => widget.onExpandChanged?.call(_toggleState));
} else if (animation.isCompleted) {
_toggleState = Expandable2State.collapsed;
widget.onExpandWillChange?.call(_toggleState);
animationController
.reverse()
.then((_) => widget.onExpandChanged?.call(_toggleState));
}
controller?.state = _toggleState;
setState(() {});
}
@override
void initState() {
controller = widget.controller;
controller?.toggle = toggle;
duration = Duration(
milliseconds: (500 * widget.animationDurationMultiplier).toInt(),
);
animationController = widget.animationController ??
AnimationController(
vsync: this,
duration: duration,
);
animation = widget.animation ??
Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
curve: Curves.easeInOut,
parent: animationController,
),
);
super.initState();
}
double _top = 0;
void getHeaderHeight() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_key.currentContext?.size?.height != null &&
_top != _key.currentContext!.size!.height) {
setState(() {
_top = _key.currentContext!.size!.height;
});
}
});
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
getHeaderHeight();
return AnimatedContainer(
duration: duration,
decoration: _toggleState == Expandable2State.expanded
? BoxDecoration(
color: widget.background,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
border: Border.all(color: widget.border),
boxShadow: [
Theme.of(context).extension<StackColors>()!.standardBoxShadow,
],
)
: BoxDecoration(
color: widget.background,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
border: Border.all(color: widget.border),
),
child: Stack(
children: [
Padding(
padding: EdgeInsets.only(top: _top),
child: SizeTransition(
sizeFactor: animation,
axisAlignment: 1.0,
child: Column(
children: widget.children
.map(
(e) => Column(
children: [
Container(
height: 1,
width: double.infinity,
color: widget.border,
),
e,
],
),
)
.toList(),
),
),
),
MouseRegion(
key: _key,
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: widget.expandOverride ?? toggle,
child: Container(
color: Colors.transparent,
child: widget.header,
),
),
),
],
),
);
}
}