mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-23 19:05:51 +00:00
desktop coin control
This commit is contained in:
parent
6c1d9d2912
commit
6f7f9c24eb
8 changed files with 990 additions and 238 deletions
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
182
lib/widgets/expandable2.dart
Normal file
182
lib/widgets/expandable2.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue