diff --git a/lib/pages/receive_view/addresses/address_card.dart b/lib/pages/receive_view/addresses/address_card.dart index 05c645352..51e1402a3 100644 --- a/lib/pages/receive_view/addresses/address_card.dart +++ b/lib/pages/receive_view/addresses/address_card.dart @@ -10,31 +10,46 @@ 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_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar/isar.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/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/pages/receive_view/addresses/address_tag.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; +import 'package:stackwallet/themes/stack_colors.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/util.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_edit_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; class AddressCard extends ConsumerStatefulWidget { const AddressCard({ - Key? key, + super.key, required this.addressId, required this.walletId, required this.coin, this.onPressed, this.clipboard = const ClipboardWrapper(), - }) : super(key: key); + }); final int addressId; final String walletId; @@ -47,6 +62,7 @@ class AddressCard extends ConsumerStatefulWidget { } class _AddressCardState extends ConsumerState { + final _qrKey = GlobalKey(); final isDesktop = Util.isDesktop; late Stream stream; @@ -54,6 +70,72 @@ class _AddressCardState extends ConsumerState { AddressLabel? label; + Future _capturePng(bool shouldSaveInsteadOfShare) async { + try { + final RenderRepaintBoundary boundary = + _qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary; + final ui.Image image = await boundary.toImage(); + final ByteData? byteData = + await image.toByteData(format: ui.ImageByteFormat.png); + final Uint8List pngBytes = byteData!.buffer.asUint8List(); + + if (shouldSaveInsteadOfShare) { + if (Util.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()) { + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "$path already exists!", + context: context, + ), + ); + } + } else { + await file.writeAsBytes(pngBytes); + if (mounted) { + 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 void initState() { address = MainDB.instance.isar.addresses @@ -117,16 +199,32 @@ class _AddressCardState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (label!.value.isNotEmpty) - Text( - label!.value, - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ), - if (label!.value.isNotEmpty) - SizedBox( - height: isDesktop ? 2 : 8, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label!.value.isNotEmpty ? label!.value : "No label", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + SimpleEditButton( + editValue: label!.value, + editLabel: 'label', + overrideTitle: 'Edit label', + disableIcon: true, + onValueChanged: (value) { + MainDB.instance.putAddressLabel( + label!.copyWith( + label: value, + ), + ); + }, + ), + ], + ), + SizedBox( + height: isDesktop ? 2 : 8, + ), Row( children: [ Expanded( @@ -140,18 +238,152 @@ class _AddressCardState extends ConsumerState { const SizedBox( height: 10, ), - if (label!.tags != null && label!.tags!.isNotEmpty) - Wrap( - spacing: 10, - runSpacing: 10, - children: label!.tags! - .map( - (e) => AddressTag( - tag: e, + Row( + children: [ + CustomTextButton( + text: "Copy address", + onTap: () { + widget.clipboard + .setData( + ClipboardData( + text: address.value, ), ) - .toList(), - ), + .then((value) { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }); + }, + ), + const SizedBox( + width: 16, + ), + CustomTextButton( + text: "Show QR code", + onTap: () async { + await showDialog( + context: context, + builder: (_) { + return StackDialogBase( + child: Column( + children: [ + if (label!.value.isNotEmpty) + Text( + label!.value, + style: STextStyles.w600_18(context), + ), + if (label!.value.isNotEmpty) + const SizedBox( + height: 8, + ), + Text( + address.value, + style: + STextStyles.w500_16(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 16, + ), + Center( + child: RepaintBoundary( + key: _qrKey, + child: QrImageView( + data: AddressUtils.buildUriString( + widget.coin, + address.value, + {}, + ), + size: 220, + backgroundColor: Theme.of(context) + .extension()! + .popupBG, + foregroundColor: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + ), + const SizedBox( + height: 16, + ), + Row( + children: [ + if (!isDesktop) + Expanded( + child: SecondaryButton( + label: "Share", + buttonHeight: isDesktop + ? ButtonHeight.l + : null, + icon: SvgPicture.asset( + Assets.svg.share, + width: 14, + height: 14, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () async { + await _capturePng(false); + }, + ), + ), + if (isDesktop) + Expanded( + child: PrimaryButton( + buttonHeight: isDesktop + ? ButtonHeight.l + : null, + onPressed: () async { + // TODO: add save functionality instead of share + // save works on linux at the moment + await _capturePng(true); + }, + label: "Save", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), + ), + ), + ], + ) + ], + ), + ); + }, + ); + }, + ), + ], + ), + // if (label!.tags != null && label!.tags!.isNotEmpty) + // Wrap( + // spacing: 10, + // runSpacing: 10, + // children: label!.tags! + // .map( + // (e) => AddressTag( + // tag: e, + // ), + // ) + // .toList(), + // ), ], ), ); diff --git a/lib/pages/receive_view/addresses/wallet_addresses_view.dart b/lib/pages/receive_view/addresses/wallet_addresses_view.dart index 597ca44fe..c6cd03215 100644 --- a/lib/pages/receive_view/addresses/wallet_addresses_view.dart +++ b/lib/pages/receive_view/addresses/wallet_addresses_view.dart @@ -10,14 +10,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/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/pages/receive_view/addresses/address_details_view.dart'; import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart'; @@ -25,13 +23,8 @@ 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/stack_text_field.dart'; import 'package:tuple/tuple.dart'; -import '../../../utilities/assets.dart'; -import '../../../widgets/icon_widgets/x_icon.dart'; -import '../../../widgets/textfield_icon_button.dart'; - class WalletAddressesView extends ConsumerStatefulWidget { const WalletAddressesView({ Key? key, @@ -50,10 +43,10 @@ class WalletAddressesView extends ConsumerStatefulWidget { class _WalletAddressesViewState extends ConsumerState { final bool isDesktop = Util.isDesktop; - String _searchString = ""; + final String _searchString = ""; - late final TextEditingController _searchController; - final searchFieldFocusNode = FocusNode(); + // late final TextEditingController _searchController; + // final searchFieldFocusNode = FocusNode(); Future> _search(String term) async { if (term.isEmpty) { @@ -119,19 +112,19 @@ class _WalletAddressesViewState extends ConsumerState { .findAll(); } - @override - void initState() { - _searchController = TextEditingController(); - - super.initState(); - } - - @override - void dispose() { - _searchController.dispose(); - searchFieldFocusNode.dispose(); - super.dispose(); - } + // @override + // void initState() { + // _searchController = TextEditingController(); + // + // super.initState(); + // } + // + // @override + // void dispose() { + // _searchController.dispose(); + // searchFieldFocusNode.dispose(); + // super.dispose(); + // } @override Widget build(BuildContext context) { @@ -165,74 +158,74 @@ class _WalletAddressesViewState extends ConsumerState { ), child: Column( children: [ - SizedBox( - width: isDesktop ? 490 : null, - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: !isDesktop, - enableSuggestions: !isDesktop, - controller: _searchController, - focusNode: searchFieldFocusNode, - onChanged: (value) { - setState(() { - _searchString = value; - }); - }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), - decoration: standardInputDecoration( - "Search...", - searchFieldFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 12 : 10, - vertical: isDesktop ? 18 : 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: isDesktop ? 20 : 16, - height: isDesktop ? 20 : 16, - ), - ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - ), - SizedBox( - height: isDesktop ? 20 : 16, - ), + // SizedBox( + // width: isDesktop ? 490 : null, + // child: ClipRRect( + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // child: TextField( + // autocorrect: !isDesktop, + // enableSuggestions: !isDesktop, + // controller: _searchController, + // focusNode: searchFieldFocusNode, + // onChanged: (value) { + // setState(() { + // _searchString = value; + // }); + // }, + // style: isDesktop + // ? STextStyles.desktopTextExtraSmall(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .textFieldActiveText, + // height: 1.8, + // ) + // : STextStyles.field(context), + // decoration: standardInputDecoration( + // "Search...", + // searchFieldFocusNode, + // context, + // desktopMed: isDesktop, + // ).copyWith( + // prefixIcon: Padding( + // padding: EdgeInsets.symmetric( + // horizontal: isDesktop ? 12 : 10, + // vertical: isDesktop ? 18 : 16, + // ), + // child: SvgPicture.asset( + // Assets.svg.search, + // width: isDesktop ? 20 : 16, + // height: isDesktop ? 20 : 16, + // ), + // ), + // suffixIcon: _searchController.text.isNotEmpty + // ? Padding( + // padding: const EdgeInsets.only(right: 0), + // child: UnconstrainedBox( + // child: Row( + // children: [ + // TextFieldIconButton( + // child: const XIcon(), + // onTap: () async { + // setState(() { + // _searchController.text = ""; + // _searchString = ""; + // }); + // }, + // ), + // ], + // ), + // ), + // ) + // : null, + // ), + // ), + // ), + // ), + // SizedBox( + // height: isDesktop ? 20 : 16, + // ), Expanded( child: FutureBuilder( future: _search(_searchString), @@ -249,15 +242,17 @@ class _WalletAddressesViewState extends ConsumerState { walletId: widget.walletId, addressId: snapshot.data![index], coin: coin, - onPressed: () { - Navigator.of(context).pushNamed( - AddressDetailsView.routeName, - arguments: Tuple2( - snapshot.data![index], - widget.walletId, - ), - ); - }, + onPressed: !isDesktop + ? null + : () { + Navigator.of(context).pushNamed( + AddressDetailsView.routeName, + arguments: Tuple2( + snapshot.data![index], + widget.walletId, + ), + ); + }, ), ); } else { diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index fab828bef..21089d30d 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -317,6 +317,28 @@ class STextStyles { } } + static TextStyle w600_18(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 18, + ); + } + } + + static TextStyle w500_16(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w500, + fontSize: 16, + ); + } + } + static TextStyle w500_14(BuildContext context) { switch (_theme(context).themeId) { default: diff --git a/lib/widgets/custom_buttons/simple_edit_button.dart b/lib/widgets/custom_buttons/simple_edit_button.dart index 26b7042b9..99d470fe3 100644 --- a/lib/widgets/custom_buttons/simple_edit_button.dart +++ b/lib/widgets/custom_buttons/simple_edit_button.dart @@ -15,29 +15,31 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/icon_widgets/pencil_icon.dart'; import 'package:tuple/tuple.dart'; -import '../desktop/desktop_dialog.dart'; -import '../icon_widgets/pencil_icon.dart'; - class SimpleEditButton extends StatelessWidget { const SimpleEditButton({ - Key? key, + super.key, this.editValue, this.editLabel, + this.overrideTitle, + this.disableIcon = false, this.onValueChanged, this.onPressedOverride, - }) : assert( + }) : assert( (editLabel != null && editValue != null && onValueChanged != null) || (editLabel == null && editValue == null && onValueChanged == null && onPressedOverride != null), - ), - super(key: key); + ); final String? editValue; final String? editLabel; + final String? overrideTitle; + final bool disableIcon; final void Function(String)? onValueChanged; final VoidCallback? onPressedOverride; @@ -101,17 +103,20 @@ class SimpleEditButton extends StatelessWidget { }, child: Row( children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context).extension()!.infoItemIcons, - ), - const SizedBox( - width: 4, - ), + if (!disableIcon) + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: + Theme.of(context).extension()!.infoItemIcons, + ), + if (!disableIcon) + const SizedBox( + width: 4, + ), Text( - "Edit", + overrideTitle ?? "Edit", style: STextStyles.link2(context), ), ],