From a22973cd35becd2722ae4ee29ff945a4f85f42e0 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 2 Feb 2023 15:37:59 -0600 Subject: [PATCH] WIP addresses list ui --- .../receive_view/addresses/address_card.dart | 132 ++++++++++ .../addresses/address_qr_popup.dart | 193 ++++++++++++++ .../addresses/edit_address_label_view.dart | 241 ++++++++++++++++++ .../receiving_addresses_view.dart | 32 +-- lib/pages/receive_view/receive_view.dart | 18 +- lib/route_generator.dart | 19 +- lib/widgets/rounded_container.dart | 3 + lib/widgets/rounded_white_container.dart | 3 + 8 files changed, 610 insertions(+), 31 deletions(-) create mode 100644 lib/pages/receive_view/addresses/address_card.dart create mode 100644 lib/pages/receive_view/addresses/address_qr_popup.dart create mode 100644 lib/pages/receive_view/addresses/edit_address_label_view.dart rename lib/pages/receive_view/{ => addresses}/receiving_addresses_view.dart (84%) diff --git a/lib/pages/receive_view/addresses/address_card.dart b/lib/pages/receive_view/addresses/address_card.dart new file mode 100644 index 000000000..1a47c256c --- /dev/null +++ b/lib/pages/receive_view/addresses/address_card.dart @@ -0,0 +1,132 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/receive_view/addresses/address_qr_popup.dart'; +import 'package:stackwallet/pages/receive_view/addresses/edit_address_label_view.dart'; +import 'package:stackwallet/utilities/clipboard_interface.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/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/copy_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:tuple/tuple.dart'; + +class AddressCard extends StatelessWidget { + const AddressCard({ + Key? key, + required this.address, + required this.walletId, + required this.coin, + this.clipboard = const ClipboardWrapper(), + }) : super(key: key); + + final Address address; + final String walletId; + final Coin coin; + final ClipboardInterface clipboard; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "TODO: label", + style: STextStyles.itemSubtitle(context), + ), + CustomTextButton( + text: "Edit label", + textSize: 14, + onTap: () { + Navigator.of(context).pushNamed( + EditAddressLabelView.routeName, + arguments: Tuple2( + address, + walletId, + ), + ); + }, + ), + ], + ), + const SizedBox( + height: 8, + ), + Row( + children: [ + Expanded( + child: SelectableText( + address.value, + style: STextStyles.itemSubtitle12(context), + ), + ) + ], + ), + const SizedBox( + height: 10, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Copy address", + icon: CopyIcon( + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () async { + await clipboard.setData( + ClipboardData( + text: address.value, + ), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + }, + ), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: SecondaryButton( + label: "Show QR Code", + icon: QrCodeIcon( + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () { + showDialog( + context: context, + builder: (context) => AddressQrPopup( + address: address, + coin: coin, + clipboard: clipboard, + ), + ); + }, + ), + ), + ], + ) + ], + ), + ); + } +} diff --git a/lib/pages/receive_view/addresses/address_qr_popup.dart b/lib/pages/receive_view/addresses/address_qr_popup.dart new file mode 100644 index 000000000..75b3509de --- /dev/null +++ b/lib/pages/receive_view/addresses/address_qr_popup.dart @@ -0,0 +1,193 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/clipboard_interface.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/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class AddressQrPopup extends StatefulWidget { + const AddressQrPopup({ + Key? key, + required this.address, + required this.coin, + this.clipboard = const ClipboardWrapper(), + }) : super(key: key); + + final Address address; + final Coin coin; + final ClipboardInterface clipboard; + + @override + State createState() => _AddressQrPopupState(); +} + +class _AddressQrPopupState extends State { + final _qrKey = GlobalKey(); + final isDesktop = Util.isDesktop; + + Future _capturePng(bool shouldSaveInsteadOfShare) async { + try { + RenderRepaintBoundary boundary = + _qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary; + ui.Image image = await boundary.toImage(); + ByteData? byteData = + await image.toByteData(format: ui.ImageByteFormat.png); + Uint8List pngBytes = byteData!.buffer.asUint8List(); + + if (shouldSaveInsteadOfShare) { + if (isDesktop) { + final dir = Directory("${Platform.environment['HOME']}"); + if (!dir.existsSync()) { + throw Exception( + "Home dir not found while trying to open filepicker on QR image save"); + } + final path = await FilePicker.platform.saveFile( + fileName: "qrcode.png", + initialDirectory: dir.path, + ); + + if (path != null) { + final file = File(path); + if (file.existsSync()) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$path already exists!", + context: context, + ), + ); + } else { + await file.writeAsBytes(pngBytes); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "$path saved!", + context: context, + ), + ); + } + } + } else { + // await DocumentFileSavePlus.saveFile( + // pngBytes, + // "receive_qr_code_${DateTime.now().toLocal().toIso8601String()}.png", + // "image/png"); + } + } else { + final tempDir = await getTemporaryDirectory(); + final file = await File("${tempDir.path}/qrcode.png").create(); + await file.writeAsBytes(pngBytes); + + await Share.shareFiles(["${tempDir.path}/qrcode.png"], + text: "Receive URI QR Code"); + } + } catch (e) { + //todo: comeback to this + debugPrint(e.toString()); + } + } + + @override + Widget build(BuildContext context) { + return StackDialogBase( + child: Column( + children: [ + Text( + "todo: custom label", + style: STextStyles.pageTitleH2(context), + ), + const SizedBox( + height: 8, + ), + Text( + widget.address.value, + style: STextStyles.itemSubtitle(context), + ), + const SizedBox( + height: 16, + ), + Center( + child: RepaintBoundary( + key: _qrKey, + child: QrImage( + data: AddressUtils.buildUriString( + widget.coin, + widget.address.value, + {}, + ), + size: 220, + backgroundColor: + Theme.of(context).extension()!.popupBG, + foregroundColor: + Theme.of(context).extension()!.accentColorDark, + ), + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + width: 170, + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + await _capturePng(false); + }, + label: "Share", + icon: SvgPicture.asset( + Assets.svg.share, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + width: 170, + onPressed: () async { + await _capturePng(true); + }, + label: "Save", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), + ), + ), + ], + ) + ], + ), + ); + } +} diff --git a/lib/pages/receive_view/addresses/edit_address_label_view.dart b/lib/pages/receive_view/addresses/edit_address_label_view.dart new file mode 100644 index 000000000..d44b61686 --- /dev/null +++ b/lib/pages/receive_view/addresses/edit_address_label_view.dart @@ -0,0 +1,241 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/isar/models/address/address.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 EditAddressLabelView extends ConsumerStatefulWidget { + const EditAddressLabelView({ + Key? key, + required this.address, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/editAddressLabel"; + + final Address address; + final String walletId; + + @override + ConsumerState createState() => + _EditAddressLabelViewState(); +} + +class _EditAddressLabelViewState extends ConsumerState { + late final TextEditingController _labelFieldController; + final labelFieldFocusNode = FocusNode(); + + late final bool isDesktop; + + @override + void initState() { + isDesktop = Util.isDesktop; + _labelFieldController = TextEditingController(); + _labelFieldController.text = "todo: address.label"; + super.initState(); + } + + @override + void dispose() { + _labelFieldController.dispose(); + labelFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: child, + ), + child: Scaffold( + backgroundColor: isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.background, + appBar: isDesktop + ? null + : 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 label", + style: STextStyles.navBarTitle(context), + ), + ), + body: ConditionalParent( + condition: !isDesktop, + builder: (child) => Padding( + padding: const EdgeInsets.all(12), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ); + }, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 12, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Edit 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: _labelFieldController, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + focusNode: labelFieldFocusNode, + decoration: standardInputDecoration( + "Address label", + labelFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: _labelFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _labelFieldController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + // if (!isDesktop) + const Spacer(), + if (isDesktop) + Padding( + padding: const EdgeInsets.all(32), + child: PrimaryButton( + label: "Save", + onPressed: () async { + // todo: update address + // await ref + // .read(notesServiceChangeNotifierProvider( + // widget.walletId)) + // .editOrAddNote( + // txid: widget.txid, + // note: _labelFieldController.text, + // ); + // if (mounted) { + // Navigator.of(context).pop(); + // } + }, + ), + ), + if (!isDesktop) + TextButton( + onPressed: () async { + // todo: update address + // await ref + // .read( + // notesServiceChangeNotifierProvider(widget.walletId)) + // .editOrAddNote( + // txid: widget.txid, + // note: _labelFieldController.text, + // ); + // if (mounted) { + // Navigator.of(context).pop(); + // } + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Save", + style: STextStyles.button(context), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/receive_view/receiving_addresses_view.dart b/lib/pages/receive_view/addresses/receiving_addresses_view.dart similarity index 84% rename from lib/pages/receive_view/receiving_addresses_view.dart rename to lib/pages/receive_view/addresses/receiving_addresses_view.dart index e5f1d88bf..927043373 100644 --- a/lib/pages/receive_view/receiving_addresses_view.dart +++ b/lib/pages/receive_view/addresses/receiving_addresses_view.dart @@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/receive_view/addresses/address_card.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -10,7 +12,6 @@ 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/loading_indicator.dart'; -import 'package:stackwallet/widgets/rounded_white_container.dart'; class ReceivingAddressesView extends ConsumerWidget { const ReceivingAddressesView({ @@ -28,6 +29,8 @@ class ReceivingAddressesView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).coin)); return ConditionalParent( condition: !isDesktop, builder: (child) => Background( @@ -73,7 +76,9 @@ class ReceivingAddressesView extends ConsumerWidget { height: 10, ), itemBuilder: (_, index) => AddressCard( + walletId: walletId, address: snapshot.data![index], + coin: coin, ), ); } else { @@ -89,28 +94,3 @@ class ReceivingAddressesView extends ConsumerWidget { ); } } - -class AddressCard extends StatelessWidget { - const AddressCard({ - Key? key, - required this.address, - }) : super(key: key); - - final Address address; - - @override - Widget build(BuildContext context) { - return RoundedWhiteContainer( - child: Row( - children: [ - Expanded( - child: Text( - address.value, - style: STextStyles.itemSubtitle12(context), - ), - ) - ], - ), - ); - } -} diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 2d7d11b45..52283111c 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -6,8 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/receive_view/addresses/receiving_addresses_view.dart'; import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; -import 'package:stackwallet/pages/receive_view/receiving_addresses_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -187,11 +187,21 @@ class _ReceiveViewState extends ConsumerState { ); }, child: RoundedWhiteContainer( + boxShadow: [ + Theme.of(context) + .extension()! + .standardBoxShadow, + ], child: Material( color: Colors.transparent, - child: Text( - "Address list", - style: STextStyles.baseXS(context), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: Text( + "Address list", + style: STextStyles.field(context), + ), ), ), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 970246043..d1ed6a4bd 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; +import 'package:stackwallet/models/isar/models/address/address.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; @@ -44,9 +45,10 @@ import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; +import 'package:stackwallet/pages/receive_view/addresses/edit_address_label_view.dart'; +import 'package:stackwallet/pages/receive_view/addresses/receiving_addresses_view.dart'; import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; -import 'package:stackwallet/pages/receive_view/receiving_addresses_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart'; @@ -474,6 +476,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case EditAddressLabelView.routeName: + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => EditAddressLabelView( + address: args.item1, + walletId: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case EditTradeNoteView.routeName: if (args is Tuple2) { return getRoute( diff --git a/lib/widgets/rounded_container.dart b/lib/widgets/rounded_container.dart index 9f9bfbd8d..d689d9ca8 100644 --- a/lib/widgets/rounded_container.dart +++ b/lib/widgets/rounded_container.dart @@ -11,6 +11,7 @@ class RoundedContainer extends StatelessWidget { this.width, this.height, this.borderColor, + this.boxShadow, }) : super(key: key); final Widget? child; @@ -20,6 +21,7 @@ class RoundedContainer extends StatelessWidget { final double? width; final double? height; final Color? borderColor; + final List? boxShadow; @override Widget build(BuildContext context) { @@ -32,6 +34,7 @@ class RoundedContainer extends StatelessWidget { Constants.size.circularBorderRadius * radiusMultiplier, ), border: borderColor == null ? null : Border.all(color: borderColor!), + boxShadow: boxShadow, ), child: Padding( padding: padding, diff --git a/lib/widgets/rounded_white_container.dart b/lib/widgets/rounded_white_container.dart index 1173e95b1..2ade9b729 100644 --- a/lib/widgets/rounded_white_container.dart +++ b/lib/widgets/rounded_white_container.dart @@ -11,6 +11,7 @@ class RoundedWhiteContainer extends StatelessWidget { this.width, this.height, this.borderColor, + this.boxShadow, }) : super(key: key); final Widget? child; @@ -19,6 +20,7 @@ class RoundedWhiteContainer extends StatelessWidget { final double? width; final double? height; final Color? borderColor; + final List? boxShadow; @override Widget build(BuildContext context) { @@ -29,6 +31,7 @@ class RoundedWhiteContainer extends StatelessWidget { width: width, height: height, borderColor: borderColor, + boxShadow: boxShadow, child: child, ); }