From e40cb3d66d1c4f71336087d23018c00f0c5bedd1 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 7 Mar 2023 11:11:57 -0600 Subject: [PATCH] utxo details ui and edit functionality --- lib/db/main_db.dart | 9 +- .../isar/models/blockchain_data/utxo.dart | 32 ++ lib/pages/coin_control/coin_control_view.dart | 9 +- lib/pages/coin_control/utxo_details_view.dart | 337 +++++++++++++++--- lib/pages/generic/single_field_edit_view.dart | 204 +++++++++++ lib/route_generator.dart | 16 + .../custom_buttons/simple_copy_button.dart | 53 +++ .../custom_buttons/simple_edit_button.dart | 55 +++ 8 files changed, 670 insertions(+), 45 deletions(-) create mode 100644 lib/pages/generic/single_field_edit_view.dart create mode 100644 lib/widgets/custom_buttons/simple_copy_button.dart create mode 100644 lib/widgets/custom_buttons/simple_edit_button.dart diff --git a/lib/db/main_db.dart b/lib/db/main_db.dart index f0a8c0c5e..c430fcd85 100644 --- a/lib/db/main_db.dart +++ b/lib/db/main_db.dart @@ -173,7 +173,14 @@ class MainDB { if (storedUtxo != null) { // update set.remove(utxo); - set.add(storedUtxo); + set.add( + storedUtxo.copyWith( + value: utxo.value, + address: utxo.address, + blockTime: utxo.blockTime, + blockHeight: utxo.blockHeight, + ), + ); } } diff --git a/lib/models/isar/models/blockchain_data/utxo.dart b/lib/models/isar/models/blockchain_data/utxo.dart index df7fb93db..d91321f9c 100644 --- a/lib/models/isar/models/blockchain_data/utxo.dart +++ b/lib/models/isar/models/blockchain_data/utxo.dart @@ -67,6 +67,38 @@ class UTXO { return confirmations >= minimumConfirms; } + UTXO copyWith({ + Id? id, + String? walletId, + String? txid, + int? vout, + int? value, + String? name, + bool? isBlocked, + String? blockedReason, + bool? isCoinbase, + String? blockHash, + int? blockHeight, + int? blockTime, + String? address, + String? otherData, + }) => + UTXO( + walletId: walletId ?? this.walletId, + txid: txid ?? this.txid, + vout: vout ?? this.vout, + value: value ?? this.value, + name: name ?? this.name, + isBlocked: isBlocked ?? this.isBlocked, + blockedReason: blockedReason ?? this.blockedReason, + isCoinbase: isCoinbase ?? this.isCoinbase, + blockHash: blockHash ?? this.blockHash, + blockHeight: blockHeight ?? this.blockHeight, + blockTime: blockTime ?? this.blockTime, + address: address ?? this.address, + otherData: otherData ?? this.otherData, + )..id = id ?? this.id; + @override String toString() => "{ " "id: $id, " diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index 2dde5b672..198a868e2 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -42,8 +42,6 @@ class _CoinControlViewState extends ConsumerState { .idProperty() .findAllSync(); - print(ids); - return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -120,14 +118,17 @@ class _CoinControlViewState extends ConsumerState { key: Key("${utxo.walletId}_${utxo.id}"), walletId: widget.walletId, utxo: utxo, - onPressed: () { - Navigator.of(context).pushNamed( + onPressed: () async { + final result = await Navigator.of(context).pushNamed( UtxoDetailsView.routeName, arguments: Tuple2( utxo.id, widget.walletId, ), ); + if (mounted && result == "refresh") { + setState(() {}); + } }, ); }, diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart index 461e982eb..60575a75f 100644 --- a/lib/pages/coin_control/utxo_details_view.dart +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; @@ -6,9 +8,15 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/providers/global/wallets_provider.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/utilities/util.dart'; import 'package:stackwallet/widgets/background.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/simple_copy_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_edit_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class UtxoDetailsView extends ConsumerStatefulWidget { @@ -28,10 +36,18 @@ class UtxoDetailsView extends ConsumerStatefulWidget { } class _UtxoDetailsViewState extends ConsumerState { - late Stream stream; + static const double _spacing = 12; + late Stream stream; UTXO? utxo; + bool _popWithRefresh = false; + + Future _toggleFreeze() async { + _popWithRefresh = true; + await MainDB.instance.putUTXO(utxo!.copyWith(isBlocked: !utxo!.isBlocked)); + } + @override void initState() { utxo = MainDB.instance.isar.utxos @@ -46,53 +62,294 @@ class _UtxoDetailsViewState extends ConsumerState { @override Widget build(BuildContext context) { - final coin = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(widget.walletId).coin)); - return Background( - child: Scaffold( - backgroundColor: Theme.of(context).extension()!.background, - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, + final coin = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).coin, + ), + ); + + final currentHeight = ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).currentHeight, + ), + ); + + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(_popWithRefresh ? "refresh" : null); + }, + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ), + ); + }, + ), ), ), - body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: StreamBuilder( - stream: stream, - builder: (context, snapshot) { - if (snapshot.hasData) { - utxo = snapshot.data!; - } + ), + child: StreamBuilder( + stream: stream, + builder: (context, snapshot) { + if (snapshot.hasData) { + utxo = snapshot.data!; + } - return Column( - children: [ - const SizedBox( - height: 10, - ), - RoundedWhiteContainer( - child: Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 10, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${Format.satoshisToAmount( + utxo!.value, + coin: coin, + ).toStringAsFixed( + coin.decimals, + )} ${coin.ticker}", + style: STextStyles.pageTitleH2(context), + ), + Text( + utxo!.isBlocked ? "Frozen" : "Available", + style: STextStyles.w500_14(context).copyWith( + color: utxo!.isBlocked + ? Theme.of(context) + .extension()! + .accentColorBlue + : Theme.of(context) + .extension()! + .accentColorGreen, + ), + ), + ], + ), + ), + const SizedBox( + height: _spacing, + ), + RoundedWhiteContainer( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "${Format.satoshisToAmount( - utxo!.value, - coin: coin, - ).toStringAsFixed( - coin.decimals, - )} ${coin.ticker}", + "Address", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + SimpleCopyButton( + data: utxo!.address!, ), ], ), - ) - ], - ); - }, - ), - ), + const SizedBox( + height: 4, + ), + Text( + utxo!.address!, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const SizedBox( + height: _spacing, + ), + RoundedWhiteContainer( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction ID", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + SimpleCopyButton( + data: utxo!.txid, + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + utxo!.txid, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const SizedBox( + height: _spacing, + ), + RoundedWhiteContainer( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Confirmations", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 4, + ), + Text( + "${currentHeight - utxo!.blockHeight!}", + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const SizedBox( + height: _spacing, + ), + RoundedWhiteContainer( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Note", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + SimpleEditButton( + editValue: utxo!.name, + editLabel: "note", + onValueChanged: (newName) { + MainDB.instance.putUTXO( + utxo!.copyWith( + name: newName, + ), + ); + }, + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + utxo!.name, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const SizedBox( + height: _spacing, + ), + if (utxo!.isBlocked) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Freeze reason", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + SimpleEditButton( + editValue: utxo!.blockedReason ?? "", + editLabel: "freeze reason", + onValueChanged: (newReason) { + MainDB.instance.putUTXO( + utxo!.copyWith( + blockedReason: newReason, + ), + ); + }, + ), + ], + ), + const SizedBox( + height: 4, + ), + Text( + utxo!.blockedReason ?? "", + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const SizedBox( + height: _spacing, + ), + ], + ), + const Spacer(), + SecondaryButton( + label: utxo!.isBlocked ? "Unfreeze" : "Freeze", + onPressed: _toggleFreeze, + ), + const SizedBox( + height: 16, + ), + ], + ); + }, ), ); } diff --git a/lib/pages/generic/single_field_edit_view.dart b/lib/pages/generic/single_field_edit_view.dart new file mode 100644 index 000000000..c1049b006 --- /dev/null +++ b/lib/pages/generic/single_field_edit_view.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_native_splash/cli_commands.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/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class SingleFieldEditView extends StatefulWidget { + const SingleFieldEditView({ + Key? key, + required this.initialValue, + required this.label, + }) : super(key: key); + + static const String routeName = "/singleFieldEdit"; + + final String initialValue; + final String label; + + @override + State createState() => _SingleFieldEditViewState(); +} + +class _SingleFieldEditViewState extends State { + late final TextEditingController _textController; + final _textFocusNode = FocusNode(); + + late final bool isDesktop; + + @override + void initState() { + isDesktop = Util.isDesktop; + _textController = TextEditingController()..text = widget.initialValue; + super.initState(); + } + + @override + void dispose() { + _textController.dispose(); + _textFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Edit ${widget.label}", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isDesktop) + const SizedBox( + height: 10, + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Edit ${widget.label}", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + ), + Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 32, + ) + : const EdgeInsets.all(0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _textController, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + focusNode: _textFocusNode, + decoration: standardInputDecoration( + widget.label.capitalize(), + _textFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: _textController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _textController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + // if (!isDesktop) + const Spacer(), + + ConditionalParent( + condition: isDesktop, + builder: (child) => Padding( + padding: const EdgeInsets.all(32), + child: child, + ), + child: PrimaryButton( + label: "Save", + onPressed: () { + if (mounted) { + Navigator.of(context).pop(_textController.text); + } + }, + ), + ) + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 5dfb42e95..7ca85d588 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -39,6 +39,7 @@ import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_4_view. import 'package:stackwallet/pages/exchange_view/send_from_view.dart'; import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; +import 'package:stackwallet/pages/generic/single_field_edit_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; @@ -201,6 +202,21 @@ class RouteGenerator { builder: (_) => const AddWalletView(), settings: RouteSettings(name: settings.name)); + case SingleFieldEditView.routeName: + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SingleFieldEditView( + initialValue: args.item1, + label: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case CoinControlView.routeName: if (args is String) { return getRoute( diff --git a/lib/widgets/custom_buttons/simple_copy_button.dart b/lib/widgets/custom_buttons/simple_copy_button.dart new file mode 100644 index 000000000..1db61db45 --- /dev/null +++ b/lib/widgets/custom_buttons/simple_copy_button.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class SimpleCopyButton extends StatelessWidget { + const SimpleCopyButton({ + Key? key, + required this.data, + }) : super(key: key); + + final String data; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () async { + await Clipboard.setData(ClipboardData(text: data)); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 10, + height: 10, + color: Theme.of(context).extension()!.infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/custom_buttons/simple_edit_button.dart b/lib/widgets/custom_buttons/simple_edit_button.dart new file mode 100644 index 000000000..af1c9259e --- /dev/null +++ b/lib/widgets/custom_buttons/simple_edit_button.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/pages/generic/single_field_edit_view.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:tuple/tuple.dart'; + +class SimpleEditButton extends StatelessWidget { + const SimpleEditButton({ + Key? key, + required this.editValue, + required this.editLabel, + required this.onValueChanged, + }) : super(key: key); + + final String editValue; + final String editLabel; + final void Function(String) onValueChanged; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () async { + final result = await Navigator.of(context).pushNamed( + SingleFieldEditView.routeName, + arguments: Tuple2( + editValue, + editLabel, + ), + ); + if (result is String && result != editValue) { + onValueChanged(result); + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: Theme.of(context).extension()!.infoItemIcons, + ), + const SizedBox( + width: 4, + ), + Text( + "Edit", + style: STextStyles.link2(context), + ), + ], + ), + ); + } +}