mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-23 11:04:33 +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;
|
||||
}
|
||||
|
||||
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/send_view/sub_widgets/sending_transaction_dialog.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/providers/providers.dart';
|
||||
import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
|
||||
|
@ -124,6 +125,7 @@ class _ConfirmTransactionViewState
|
|||
]);
|
||||
|
||||
txid = results.first as String;
|
||||
ref.refresh(desktopUseUTXOs);
|
||||
|
||||
// save note
|
||||
await ref
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:decimal/decimal.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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/constants.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/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/dropdown_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/expandable2.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/rounded_container.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/toggle.dart';
|
||||
|
||||
final desktopUseUTXOs = StateProvider.autoDispose((ref) => <UTXO>{});
|
||||
final desktopUseUTXOs = StateProvider((ref) => <UTXO>{});
|
||||
|
||||
class DesktopCoinControlUseDialog extends ConsumerStatefulWidget {
|
||||
const DesktopCoinControlUseDialog({
|
||||
Key? key,
|
||||
required this.walletId,
|
||||
this.amountToSend,
|
||||
}) : super(key: key);
|
||||
|
||||
final String walletId;
|
||||
final Decimal? amountToSend;
|
||||
|
||||
@override
|
||||
ConsumerState<DesktopCoinControlUseDialog> createState() =>
|
||||
|
@ -42,13 +50,22 @@ class _DesktopCoinControlUseDialogState
|
|||
late final Coin coin;
|
||||
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 = "";
|
||||
|
||||
CCFilter _filter = CCFilter.available;
|
||||
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
|
||||
void initState() {
|
||||
_searchController = TextEditingController();
|
||||
|
@ -56,6 +73,13 @@ class _DesktopCoinControlUseDialogState
|
|||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.coin;
|
||||
|
||||
for (final utxo in ref.read(desktopUseUTXOs)) {
|
||||
final data = UtxoRowData(utxo.id, true);
|
||||
_selectedUTXOs.add(utxo);
|
||||
_selectedUTXOsData.add(data);
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -68,14 +92,40 @@ class _DesktopCoinControlUseDialogState
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ids = MainDB.instance.queryUTXOsSync(
|
||||
walletId: widget.walletId,
|
||||
filter: _filter,
|
||||
sort: _sort,
|
||||
searchTerm: _searchString,
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
|
||||
if (_sort == CCSortDescriptor.address) {
|
||||
_list = null;
|
||||
_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,
|
||||
);
|
||||
|
||||
final enableApply = widget.amountToSend == null
|
||||
? selectedChanged(_selectedUTXOs)
|
||||
: selectedChanged(_selectedUTXOs) &&
|
||||
widget.amountToSend! <= selectedSum;
|
||||
|
||||
return DesktopDialog(
|
||||
maxWidth: 700,
|
||||
maxHeight: MediaQuery.of(context).size.height - 128,
|
||||
|
@ -220,75 +270,165 @@ class _DesktopCoinControlUseDialogState
|
|||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
SizedBox(
|
||||
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: () {},
|
||||
child: SvgPicture.asset(
|
||||
Assets.svg.list,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
JDropdownIconButton(
|
||||
redrawOnScreenSizeChanged: true,
|
||||
groupValue: _sort,
|
||||
items: CCSortDescriptor.values.toSet(),
|
||||
onSelectionChanged: (CCSortDescriptor? newValue) {
|
||||
if (newValue != null && newValue != _sort) {
|
||||
setState(() {
|
||||
_sort = newValue;
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
itemCount: ids.length,
|
||||
separatorBuilder: (context, _) => const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final utxo = MainDB.instance.isar.utxos
|
||||
.where()
|
||||
.idEqualTo(ids[index])
|
||||
.findFirstSync()!;
|
||||
final data = UtxoRowData(utxo.id, false);
|
||||
data.selected = _selectedUTXOs.contains(data);
|
||||
child: _list != null
|
||||
? ListView.separated(
|
||||
shrinkWrap: true,
|
||||
primary: false,
|
||||
itemCount: _list!.length,
|
||||
separatorBuilder: (context, _) => const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final utxo = MainDB.instance.isar.utxos
|
||||
.where()
|
||||
.idEqualTo(_list![index])
|
||||
.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,
|
||||
walletId: widget.walletId,
|
||||
onSelectionChanged: (value) {
|
||||
setState(() {
|
||||
if (data.selected) {
|
||||
_selectedUTXOs.add(value);
|
||||
} else {
|
||||
_selectedUTXOs.remove(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
return UtxoRow(
|
||||
key: Key(
|
||||
"${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"),
|
||||
data: data,
|
||||
compact: true,
|
||||
walletId: widget.walletId,
|
||||
onSelectionChanged: (value) {
|
||||
setState(() {
|
||||
if (data.selected) {
|
||||
_selectedUTXOsData.add(value);
|
||||
_selectedUTXOs.add(utxo);
|
||||
} 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(
|
||||
height: 16,
|
||||
|
@ -297,29 +437,95 @@ class _DesktopCoinControlUseDialogState
|
|||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
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,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
child: ConditionalParent(
|
||||
condition: widget.amountToSend != null,
|
||||
builder: (child) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
child,
|
||||
Container(
|
||||
height: 1.2,
|
||||
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(
|
||||
|
@ -329,14 +535,17 @@ class _DesktopCoinControlUseDialogState
|
|||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
enabled: _selectedUTXOs.isNotEmpty,
|
||||
enabled: _selectedUTXOsData.isNotEmpty,
|
||||
buttonHeight: ButtonHeight.l,
|
||||
label: _selectedUTXOs.isEmpty
|
||||
label: _selectedUTXOsData.isEmpty
|
||||
? "Clear selection"
|
||||
: "Clear selection (${_selectedUTXOs.length})",
|
||||
onPressed: () => setState(() {
|
||||
_selectedUTXOs.clear();
|
||||
}),
|
||||
: "Clear selection (${_selectedUTXOsData.length})",
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_selectedUTXOsData.clear();
|
||||
_selectedUTXOs.clear();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
|
@ -344,9 +553,15 @@ class _DesktopCoinControlUseDialogState
|
|||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
enabled: _selectedUTXOs.isNotEmpty,
|
||||
enabled: enableApply,
|
||||
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/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/dropdown_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/expandable2.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/rounded_container.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
|
@ -44,6 +47,9 @@ class _DesktopCoinControlViewState
|
|||
|
||||
final Set<UtxoRowData> _selectedUTXOs = {};
|
||||
|
||||
Map<String, List<Id>>? _map;
|
||||
List<Id>? _list;
|
||||
|
||||
String _searchString = "";
|
||||
|
||||
CCFilter _filter = CCFilter.all;
|
||||
|
@ -70,13 +76,25 @@ class _DesktopCoinControlViewState
|
|||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
|
||||
final ids = MainDB.instance.queryUTXOsSync(
|
||||
walletId: widget.walletId,
|
||||
filter: _filter,
|
||||
sort: _sort,
|
||||
searchTerm: _searchString,
|
||||
coin: coin,
|
||||
);
|
||||
if (_sort == CCSortDescriptor.address) {
|
||||
_list = null;
|
||||
_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,
|
||||
);
|
||||
}
|
||||
|
||||
return DesktopScaffold(
|
||||
appBar: DesktopAppBar(
|
||||
|
@ -264,35 +282,135 @@ class _DesktopCoinControlViewState
|
|||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
child: ListView.separated(
|
||||
itemCount: ids.length,
|
||||
separatorBuilder: (context, _) => const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final utxo = MainDB.instance.isar.utxos
|
||||
.where()
|
||||
.idEqualTo(ids[index])
|
||||
.findFirstSync()!;
|
||||
final data = UtxoRowData(utxo.id, false);
|
||||
data.selected = _selectedUTXOs.contains(data);
|
||||
child: _list != null
|
||||
? 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 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,
|
||||
onSelectionChanged: (value) {
|
||||
setState(() {
|
||||
if (data.selected) {
|
||||
_selectedUTXOs.add(value);
|
||||
} else {
|
||||
_selectedUTXOs.remove(value);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
return UtxoRow(
|
||||
key: Key(
|
||||
"${utxo.walletId}_${utxo.id}_${utxo.isBlocked}"),
|
||||
data: data,
|
||||
walletId: widget.walletId,
|
||||
onSelectionChanged: (value) {
|
||||
setState(() {
|
||||
if (data.selected) {
|
||||
_selectedUTXOs.add(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,
|
||||
this.onSelectionChanged,
|
||||
this.compact = false,
|
||||
this.compactWithBorder = true,
|
||||
this.raiseOnSelected = true,
|
||||
}) : super(key: key);
|
||||
|
||||
final String walletId;
|
||||
final UtxoRowData data;
|
||||
final void Function(UtxoRowData)? onSelectionChanged;
|
||||
final bool compact;
|
||||
final bool compactWithBorder;
|
||||
final bool raiseOnSelected;
|
||||
|
||||
@override
|
||||
ConsumerState<UtxoRow> createState() => _UtxoRowState();
|
||||
|
@ -96,29 +100,31 @@ class _UtxoRowState extends ConsumerState<UtxoRow> {
|
|||
}
|
||||
|
||||
return RoundedContainer(
|
||||
borderColor: widget.compact
|
||||
borderColor: widget.compact && widget.compactWithBorder
|
||||
? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG
|
||||
: null,
|
||||
color: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||
boxShadow: widget.data.selected
|
||||
boxShadow: widget.data.selected && widget.raiseOnSelected
|
||||
? [
|
||||
Theme.of(context).extension<StackColors>()!.standardBoxShadow,
|
||||
]
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: widget.data.selected,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
widget.data.selected = value!;
|
||||
});
|
||||
widget.onSelectionChanged?.call(widget.data);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
if (!(widget.compact && utxo.isBlocked))
|
||||
Checkbox(
|
||||
value: widget.data.selected,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
widget.data.selected = value!;
|
||||
});
|
||||
widget.onSelectionChanged?.call(widget.data);
|
||||
},
|
||||
),
|
||||
if (!(widget.compact && utxo.isBlocked))
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
UTXOStatusIcon(
|
||||
blocked: utxo.isBlocked,
|
||||
status: utxo.isConfirmed(
|
||||
|
|
|
@ -122,93 +122,101 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin);
|
||||
}
|
||||
|
||||
// confirm send all
|
||||
if (amount == availableBalance) {
|
||||
final bool? shouldSendAll = await showDialog<bool>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return DesktopDialog(
|
||||
maxWidth: 450,
|
||||
maxHeight: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 32,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Confirm send all",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
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(
|
||||
final coinControlEnabled =
|
||||
ref.read(prefsChangeNotifierProvider).enableCoinControl;
|
||||
|
||||
if (!(manager.hasCoinControlSupport && coinControlEnabled) ||
|
||||
(manager.hasCoinControlSupport &&
|
||||
coinControlEnabled &&
|
||||
ref.read(desktopUseUTXOs).isEmpty)) {
|
||||
// confirm send all
|
||||
if (amount == availableBalance) {
|
||||
final bool? shouldSendAll = await showDialog<bool>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return DesktopDialog(
|
||||
maxWidth: 450,
|
||||
maxHeight: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 32,
|
||||
bottom: 32,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
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);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
"Confirm send all",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
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) {
|
||||
// cancel preview
|
||||
return;
|
||||
if (shouldSendAll == null || shouldSendAll == false) {
|
||||
// cancel preview
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,7 +269,14 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
txDataFuture = wallet.preparePaymentCodeSend(
|
||||
paymentCode: paymentCode,
|
||||
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) &&
|
||||
ref.read(publicPrivateBalanceStateProvider.state).state !=
|
||||
|
@ -269,13 +284,27 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic(
|
||||
address: _address!,
|
||||
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 {
|
||||
txDataFuture = manager.prepareSend(
|
||||
address: _address!,
|
||||
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 {
|
||||
if (_amountToSend == null) {
|
||||
//
|
||||
}
|
||||
final result = await showDialog(
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => DesktopCoinControlUseDialog(
|
||||
builder: (context) => DesktopCoinControlUseDialog(
|
||||
walletId: widget.walletId,
|
||||
amountToSend: _amountToSend,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1094,7 +1121,9 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
style: STextStyles.desktopTextExtraExtraSmall(context),
|
||||
),
|
||||
CustomTextButton(
|
||||
text: "Select coins",
|
||||
text: ref.watch(desktopUseUTXOs.state).state.isEmpty
|
||||
? "Select coins"
|
||||
: "Selected coins (${ref.watch(desktopUseUTXOs.state).state.length})",
|
||||
onTap: _showDesktopCoinControl,
|
||||
),
|
||||
],
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart';
|
||||
|
@ -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 {
|
||||
|
|
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