From 1e05a8dfa3df395f26ad709619ceeb4f659be3e6 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 27 Jul 2023 08:37:08 -0600 Subject: [PATCH 1/7] desktop ordinals details view info layout --- .../desktop_ordinal_details_view.dart | 176 +++++++++--------- 1 file changed, 84 insertions(+), 92 deletions(-) diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index 2f89b7cd6..b206d71b5 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -1,12 +1,9 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/models/isar/ordinal.dart'; -import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -137,7 +134,7 @@ class _DesktopOrdinalDetailsViewState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + SelectableText( "INSC. ${widget.ordinal.inscriptionNumber}", style: STextStyles.w600_20(context), ), @@ -197,51 +194,48 @@ class _DesktopOrdinalDetailsViewState const SizedBox( height: 16, ), - _DetailsItemWCopy( - title: "Inscription number", - data: widget.ordinal.inscriptionNumber.toString(), - ), - const SizedBox( - height: _spacing, - ), - _DetailsItemWCopy( - title: "Inscription ID", - data: widget.ordinal.inscriptionId, - ), - const SizedBox( - height: _spacing, - ), - // todo: add utxo status - const SizedBox( - height: _spacing, - ), - _DetailsItemWCopy( - title: "Amount", - data: utxo == null - ? "ERROR" - : ref.watch(pAmountFormatter(coin)).format( - Amount( - rawValue: BigInt.from(utxo!.value), - fractionDigits: coin.decimals, - ), - ), - ), - const SizedBox( - height: _spacing, - ), - _DetailsItemWCopy( - title: "Owner address", - data: utxo?.address ?? "ERROR", - ), - const SizedBox( - height: _spacing, - ), - _DetailsItemWCopy( - title: "Transaction ID", - data: widget.ordinal.utxoTXID, - ), - const SizedBox( - height: _spacing, + RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _DetailsItemWCopy( + title: "Inscription number", + data: widget.ordinal.inscriptionNumber.toString(), + ), + const _Divider(), + _DetailsItemWCopy( + title: "Inscription ID", + data: widget.ordinal.inscriptionId, + ), + // const SizedBox( + // height: _spacing, + // ), + // // todo: add utxo status + const _Divider(), + _DetailsItemWCopy( + title: "Amount", + data: utxo == null + ? "ERROR" + : ref.watch(pAmountFormatter(coin)).format( + Amount( + rawValue: BigInt.from(utxo!.value), + fractionDigits: coin.decimals, + ), + ), + ), + const _Divider(), + _DetailsItemWCopy( + title: "Owner address", + data: utxo?.address ?? "ERROR", + ), + const _Divider(), + _DetailsItemWCopy( + title: "Transaction ID", + data: widget.ordinal.utxoTXID, + ), + ], + ), ), ], ), @@ -255,6 +249,23 @@ class _DesktopOrdinalDetailsViewState } } +class _Divider extends StatelessWidget { + const _Divider({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + child: Container( + height: 1, + color: Theme.of(context).extension()!.backgroundAppBar, + ), + ); + } +} + class _DetailsItemWCopy extends StatelessWidget { const _DetailsItemWCopy({ Key? key, @@ -267,48 +278,29 @@ class _DetailsItemWCopy extends StatelessWidget { @override Widget build(BuildContext context) { - return RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: STextStyles.itemSubtitle(context), - ), - GestureDetector( - onTap: () async { - await Clipboard.setData(ClipboardData(text: data)); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), - ); - } - }, - child: SvgPicture.asset( - Assets.svg.copy, - color: - Theme.of(context).extension()!.infoItemIcons, - width: 12, - ), - ), - ], - ), - const SizedBox( - height: 4, - ), - SelectableText( - data, - style: STextStyles.itemSubtitle12(context), - ), - ], - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: STextStyles.itemSubtitle(context), + ), + IconCopyButton( + data: data, + ), + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + data, + style: STextStyles.itemSubtitle12(context), + ), + ], ); } } From 2130b76788edc2288ff5144694472d4a9e886a1d Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 27 Jul 2023 08:39:32 -0600 Subject: [PATCH 2/7] monero ref --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 407425c9f..e48952185 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 407425c9fcf7a30c81f1345246c7225bc18b5cd5 +Subproject commit e48952185556a10f182184fd572bcb04365f5831 From 8b88890e05d93226baf32f0f3c4e55a6a2b8b4fd Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 27 Jul 2023 09:28:01 -0600 Subject: [PATCH 3/7] refactor network call into mockable provider and handle errors --- lib/pages/monkey/monkey_view.dart | 309 ++++++++++++++---------------- lib/services/monkey_service.dart | 40 ++++ 2 files changed, 187 insertions(+), 162 deletions(-) create mode 100644 lib/services/monkey_service.dart diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 2ad4b18eb..48b307bab 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -4,11 +4,12 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; +import 'package:stackwallet/services/monkey_service.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -44,75 +45,14 @@ class _MonkeyViewState extends ConsumerState { late final String walletId; List? imageBytes; - String receivingAddress = ""; - - Future getMonkeyImage(String address) async { - if (address.isEmpty) { - //address shouldn't be empty - return; - } - - final http.Response response = await http - .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); - - if (response.statusCode == 200) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); - final decodedResponse = response.bodyBytes; - await (manager.wallet as BananoWallet) - .updateMonkeyImageBytes(decodedResponse); - } else { - throw Exception("Failed to get MonKey"); - } + Future _updateWalletMonKey(Uint8List monKeyBytes) async { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + await (manager.wallet as BananoWallet) + .updateMonkeyImageBytes(monKeyBytes.toList()); } - // void getMonkeySVG(String address) async { - // if (address.isEmpty) { - // //address shouldn't be empty - // return; - // } - // - // final http.Response response = await http - // .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); - // - // if (response.statusCode == 200) { - // final decodedResponse = response.bodyBytes; - // Directory directory = await getApplicationDocumentsDirectory(); - // late Directory sampleFolder; - // - // if (Platform.isAndroid) { - // directory = Directory("/storage/emulated/0/"); - // sampleFolder = Directory('${directory!.path}Documents'); - // } else if (Platform.isIOS) { - // sampleFolder = Directory(directory!.path); - // } else if (Platform.isLinux) { - // sampleFolder = Directory('${directory!.path}Documents'); - // } else if (Platform.isWindows) { - // sampleFolder = Directory('${directory!.path}Documents'); - // } else if (Platform.isMacOS) { - // sampleFolder = Directory('${directory!.path}Documents'); - // } - // - // try { - // if (!sampleFolder.existsSync()) { - // sampleFolder.createSync(recursive: true); - // } - // } catch (e, s) { - // // todo: come back to this - // debugPrint("$e $s"); - // } - // - // final docPath = sampleFolder.path; - // final filePath = "$docPath/monkey.svg"; - // - // File imgFile = File(filePath); - // await imgFile.writeAsBytes(decodedResponse); - // } else { - // throw Exception("Failed to get MonKey"); - // } - // } - - Future getDocsDir() async { + Future _getDocsDir() async { try { if (Platform.isAndroid) { return Directory("/storage/emulated/0/Documents"); @@ -124,80 +64,45 @@ class _MonkeyViewState extends ConsumerState { } } - Future downloadMonkey(String address, bool isPNG) async { - if (address.isEmpty) { - //address shouldn't be empty - return; + Future _saveMonKeyToFile({ + required Uint8List bytes, + bool isPNG = false, + bool overwrite = false, + }) async { + if (Platform.isAndroid) { + await Permission.storage.request(); } - String url = "https://monkey.banano.cc/api/v1/monkey/$address"; - - if (isPNG) { - url += '?format=png&size=512&background=false'; + final dir = await _getDocsDir(); + if (dir == null) { + throw Exception("Failed to get documents directory to save monKey image"); } - final http.Response response = await http.get(Uri.parse(url)); + final address = await ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .currentReceivingAddress; + final docPath = dir.path; + String filePath = "$docPath/monkey_$address"; - if (response.statusCode == 200) { - if (Platform.isAndroid) { - await Permission.storage.request(); - } + filePath += isPNG ? ".png" : ".svg"; - final decodedResponse = response.bodyBytes; - final Directory? sampleFolder = await getDocsDir(); + File imgFile = File(filePath); - print("PATH: ${sampleFolder?.path}"); - - if (sampleFolder == null) { - print("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - return; - } - - // try { - // if (!sampleFolder.existsSync()) { - // sampleFolder.createSync(recursive: true); - // } - // } catch (e, s) { - // // todo: come back to this - // debugPrint("$e $s"); - // } - - final docPath = sampleFolder.path; - String filePath = "$docPath/monkey_$address"; - - filePath += isPNG ? ".png" : ".svg"; - - // todo check if monkey.png exists - - File imgFile = File(filePath); - await imgFile.writeAsBytes(decodedResponse); - } else { - throw Exception("Failed to get MonKey"); + if (imgFile.existsSync() && !overwrite) { + throw Exception("File already exists"); } + + await imgFile.writeAsBytes(bytes); } @override void initState() { walletId = widget.walletId; - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - final address = await ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .currentReceivingAddress; - setState(() { - receivingAddress = address; - }); - }); - super.initState(); } - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { final manager = ref.watch(walletsChangeNotifierProvider @@ -318,22 +223,24 @@ class _MonkeyViewState extends ConsumerState { AspectRatio( aspectRatio: 1, child: AppBarIconButton( - icon: SvgPicture.asset( - Assets.svg.circleQuestion, - ), - onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const StackOkDialog( - title: "About MonKeys", - message: - "A MonKey is a visual representation of your Banano address.", - ); - }); - }), + icon: SvgPicture.asset( + Assets.svg.circleQuestion, + ), + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const StackOkDialog( + title: "About MonKeys", + message: + "A MonKey is a visual representation of your Banano address.", + ); + }, + ); + }, + ), ), ], ), @@ -376,26 +283,95 @@ class _MonkeyViewState extends ConsumerState { SecondaryButton( label: "Save as SVG", onPressed: () async { + bool didError = false; await showLoading( - whileFuture: - downloadMonkey(receivingAddress, false), + whileFuture: Future.wait([ + _saveMonKeyToFile( + bytes: Uint8List.fromList( + (manager.wallet as BananoWallet) + .getMonkeyImageBytes()!), + ), + Future.delayed( + const Duration(seconds: 2), + ), + ]), context: context, isDesktop: Util.isDesktop, message: "Saving MonKey svg", + onException: (e) { + didError = true; + String msg = e.toString(); + while (msg.isNotEmpty && + msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }, ); + + if (!didError && mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: "SVG MonKey image saved", + context: context, + ); + } }, ), const SizedBox(height: 12), SecondaryButton( label: "Download as PNG", onPressed: () async { + bool didError = false; await showLoading( - whileFuture: - downloadMonkey(receivingAddress, true), + whileFuture: Future.wait([ + manager.currentReceivingAddress.then( + (address) async => await ref + .read(pMonKeyService) + .fetchMonKey( + address: address, + png: true, + ) + .then( + (monKeyBytes) async => + await _saveMonKeyToFile( + bytes: monKeyBytes, + isPNG: true, + ), + ), + ), + Future.delayed( + const Duration(seconds: 2)), + ]), context: context, isDesktop: Util.isDesktop, message: "Downloading MonKey png", + onException: (e) { + didError = true; + String msg = e.toString(); + while (msg.isNotEmpty && + msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }, ); + + if (!didError && mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: "PNG MonKey image saved", + context: context, + ); + } }, ), ], @@ -453,17 +429,37 @@ class _MonkeyViewState extends ConsumerState { child: PrimaryButton( label: "Fetch MonKey", onPressed: () async { - final future = Future.wait([ - getMonkeyImage(receivingAddress), - Future.delayed(const Duration(seconds: 2)), - ]); - await showLoading( - whileFuture: future, + whileFuture: Future.wait([ + manager.currentReceivingAddress.then( + (address) async => await ref + .read(pMonKeyService) + .fetchMonKey(address: address) + .then( + (monKeyBytes) async => + await _updateWalletMonKey( + monKeyBytes, + ), + ), + ), + Future.delayed(const Duration(seconds: 2)), + ]), context: context, isDesktop: Util.isDesktop, message: "Fetching MonKey", subMessage: "We are fetching your MonKey", + onException: (e) { + String msg = e.toString(); + while (msg.isNotEmpty && + msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }, ); imageBytes = (manager.wallet as BananoWallet) @@ -472,17 +468,6 @@ class _MonkeyViewState extends ConsumerState { if (imageBytes != null) { setState(() {}); } - - // if (isDesktop) { - // Navigator.of(context).popUntil( - // ModalRoute.withName( - // DesktopWalletView.routeName), - // ); - // } else { - // Navigator.of(context).popUntil( - // ModalRoute.withName(WalletView.routeName), - // ); - // } }, ), ), diff --git a/lib/services/monkey_service.dart b/lib/services/monkey_service.dart new file mode 100644 index 000000000..46dfbb0ef --- /dev/null +++ b/lib/services/monkey_service.dart @@ -0,0 +1,40 @@ +import 'dart:typed_data'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart' as http; +import 'package:stackwallet/utilities/logger.dart'; + +final pMonKeyService = Provider((ref) => MonKeyService()); + +class MonKeyService { + static const baseURL = "https://monkey.banano.cc/api/v1/monkey/"; + + Future fetchMonKey({ + required String address, + bool png = false, + }) async { + try { + String url = "https://monkey.banano.cc/api/v1/monkey/$address"; + + if (png) { + url += '?format=png&size=512&background=false'; + } + + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + return response.bodyBytes; + } else { + throw Exception( + "statusCode=${response.statusCode} body=${response.body}", + ); + } + } catch (e, s) { + Logging.instance.log( + "Failed fetchMonKey($address): $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } +} From 6ccd7a76a50d2d2dbc602f8825c5bbf21b9ddbc4 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 27 Jul 2023 09:47:50 -0600 Subject: [PATCH 4/7] desktop clean up --- lib/pages/monkey/monkey_view.dart | 141 ++++++++++++------ .../ordinals/desktop_ordinals_view.dart | 7 +- 2 files changed, 99 insertions(+), 49 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 48b307bab..85ee8d5f2 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -21,6 +21,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/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; @@ -113,7 +115,6 @@ class _MonkeyViewState extends ConsumerState { imageBytes ??= (manager.wallet as BananoWallet).getMonkeyImageBytes(); - //edit for desktop return Background( child: ConditionalParent( condition: isDesktop, @@ -140,64 +141,116 @@ class _MonkeyViewState extends ConsumerState { .extension()! .topNavIconPrimary, ), - onPressed: () { - if (mounted) { - Navigator.of(context).pop(); - } - }, + onPressed: Navigator.of(context).pop, ), const SizedBox( width: 15, ), - SvgPicture.asset(Assets.svg.monkey), + SvgPicture.asset( + Assets.svg.monkey, + width: 32, + height: 32, + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), const SizedBox( width: 12, ), Text( "MonKey", - style: STextStyles.navBarTitle(context), + style: STextStyles.desktopH3(context), ), ], ), ), - trailing: Padding( - padding: const EdgeInsets.all(8.0), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const StackDialog( - title: "About MonKeys", - message: - "A MonKey is a visual representation of your Banano address.", - ); - }); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.circleQuestion, - color: Colors.blue[800], - ), - const SizedBox( - width: 6, - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "What is MonKey?", - style: STextStyles.desktopTextSmall(context).copyWith( - color: Colors.blue[800], + trailing: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(1000), + ), + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "About MonKeys", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], ), - ), + Text( + "A MonKey is a visual representation of your Banano address.", + style: + STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.all( + 32, + ), + child: PrimaryButton( + width: 272.5, + label: "OK", + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ], ), - ], - ), + ); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 19, + horizontal: 32, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circleQuestion, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + const SizedBox( + width: 8, + ), + Text( + "What is MonKey?", + style: + STextStyles.desktopMenuItemSelected(context).copyWith( + color: Theme.of(context) + .extension()! + .customTextButtonEnabledText, + ), + ) + ], ), ), ), diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index ec2748560..e919ff299 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -16,17 +16,12 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_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/icon_widgets/x_icon.dart'; -import 'package:stackwallet/widgets/stack_text_field.dart'; -import 'package:stackwallet/widgets/textfield_icon_button.dart'; class DesktopOrdinalsView extends ConsumerStatefulWidget { const DesktopOrdinalsView({ @@ -102,6 +97,8 @@ class _DesktopOrdinals extends ConsumerState { Assets.svg.ordinal, width: 32, height: 32, + color: + Theme.of(context).extension()!.textSubtitle1, ), const SizedBox( width: 12, From e93ecad7441cb25e7bcc1f0169658ddd6fb13a7a Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 27 Jul 2023 10:26:52 -0600 Subject: [PATCH 5/7] save ordinal image to file --- lib/pages/ordinals/ordinal_details_view.dart | 94 +++++++++++++--- .../desktop_ordinal_details_view.dart | 103 +++++++++++++++--- 2 files changed, 167 insertions(+), 30 deletions(-) diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index ced80c42c..6bbd0730e 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -1,17 +1,21 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:http/http.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_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'; @@ -184,6 +188,36 @@ class _OrdinalImageGroup extends StatelessWidget { static const _spacing = 12.0; + Future _savePngToFile() async { + final response = await get(Uri.parse(ordinal.content)); + + if (response.statusCode != 200) { + throw Exception( + "statusCode=${response.statusCode} body=${response.bodyBytes}"); + } + + final bytes = response.bodyBytes; + + if (Platform.isAndroid) { + await Permission.storage.request(); + } + + final dir = Platform.isAndroid + ? Directory("/storage/emulated/0/Documents") + : await getApplicationDocumentsDirectory(); + + final docPath = dir.path; + final filePath = "$docPath/ordinal_${ordinal.inscriptionNumber}.png"; + + File imgFile = File(filePath); + + if (imgFile.existsSync()) { + throw Exception("File already exists"); + } + + await imgFile.writeAsBytes(bytes); + } + @override Widget build(BuildContext context) { return Column( @@ -197,15 +231,20 @@ class _OrdinalImageGroup extends StatelessWidget { // const SizedBox( // height: _spacing, // ), - AspectRatio( - aspectRatio: 1, - child: Container( - color: Colors.transparent, - child: Image.network( - ordinal.content, // Use the preview URL as the image source - fit: BoxFit.cover, - filterQuality: - FilterQuality.none, // Set the filter mode to nearest + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: AspectRatio( + aspectRatio: 1, + child: Container( + color: Colors.transparent, + child: Image.network( + ordinal.content, // Use the preview URL as the image source + fit: BoxFit.cover, + filterQuality: + FilterQuality.none, // Set the filter mode to nearest + ), ), ), ), @@ -227,8 +266,37 @@ class _OrdinalImageGroup extends StatelessWidget { ), buttonHeight: ButtonHeight.l, iconSpacing: 4, - onPressed: () { - // TODO: save and download image to device + onPressed: () async { + bool didError = false; + await showLoading( + whileFuture: Future.wait([ + _savePngToFile(), + Future.delayed(const Duration(seconds: 2)), + ]), + context: context, + isDesktop: true, + message: "Saving ordinal image", + onException: (e) { + didError = true; + String msg = e.toString(); + while (msg.isNotEmpty && msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }, + ); + + if (!didError && context.mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: "Image saved", + context: context, + ); + } }, ), ), diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index b206d71b5..0ec19a19f 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -1,8 +1,14 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:http/http.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/models/isar/ordinal.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; @@ -12,10 +18,12 @@ import 'package:stackwallet/utilities/amount/amount_formatter.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/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_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/rounded_white_container.dart'; class DesktopOrdinalDetailsView extends ConsumerStatefulWidget { @@ -41,6 +49,36 @@ class _DesktopOrdinalDetailsViewState late final UTXO? utxo; + Future _savePngToFile() async { + final response = await get(Uri.parse(widget.ordinal.content)); + + if (response.statusCode != 200) { + throw Exception( + "statusCode=${response.statusCode} body=${response.bodyBytes}"); + } + + final bytes = response.bodyBytes; + + if (Platform.isAndroid) { + await Permission.storage.request(); + } + + final dir = Platform.isAndroid + ? Directory("/storage/emulated/0/Documents") + : await getApplicationDocumentsDirectory(); + + final docPath = dir.path; + final filePath = "$docPath/ordinal_${widget.ordinal.inscriptionNumber}.png"; + + File imgFile = File(filePath); + + if (imgFile.existsSync()) { + throw Exception("File already exists"); + } + + await imgFile.writeAsBytes(bytes); + } + @override void initState() { utxo = widget.ordinal.getUTXO(ref.read(mainDBProvider)); @@ -171,23 +209,54 @@ class _DesktopOrdinalDetailsViewState // const SizedBox( // width: 16, // ), - // SecondaryButton( - // width: 150, - // label: "Download", - // icon: SvgPicture.asset( - // Assets.svg.arrowDown, - // width: 13, - // height: 18, - // color: Theme.of(context) - // .extension()! - // .buttonTextSecondary, - // ), - // buttonHeight: ButtonHeight.l, - // iconSpacing: 8, - // onPressed: () { - // // TODO: save and download image to device - // }, - // ), + SecondaryButton( + width: 150, + label: "Download", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 13, + height: 18, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + buttonHeight: ButtonHeight.l, + iconSpacing: 8, + onPressed: () async { + bool didError = false; + await showLoading( + whileFuture: Future.wait([ + _savePngToFile(), + Future.delayed( + const Duration(seconds: 2)), + ]), + context: context, + isDesktop: true, + message: "Saving ordinal image", + onException: (e) { + didError = true; + String msg = e.toString(); + while (msg.isNotEmpty && + msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }, + ); + + if (!didError && mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: "Image saved", + context: context, + ); + } + }, + ), ], ), ), From 7ca6da60f9109b03234e3522fc93043b4aba61f4 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 27 Jul 2023 10:33:59 -0600 Subject: [PATCH 6/7] clean up mobile ordinal details view --- lib/pages/ordinals/ordinal_details_view.dart | 75 +++++++++++++++----- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index 6bbd0730e..2057c91db 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -3,15 +3,22 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:http/http.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/amount/amount_formatter.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/show_loading.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -19,7 +26,7 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -class OrdinalDetailsView extends StatefulWidget { +class OrdinalDetailsView extends ConsumerStatefulWidget { const OrdinalDetailsView({ Key? key, required this.walletId, @@ -32,14 +39,25 @@ class OrdinalDetailsView extends StatefulWidget { static const routeName = "/ordinalDetailsView"; @override - State createState() => _OrdinalDetailsViewState(); + ConsumerState createState() => _OrdinalDetailsViewState(); } -class _OrdinalDetailsViewState extends State { +class _OrdinalDetailsViewState extends ConsumerState { static const _spacing = 12.0; + late final UTXO? utxo; + + @override + void initState() { + utxo = widget.ordinal.getUTXO(ref.read(mainDBProvider)); + super.initState(); + } + @override Widget build(BuildContext context) { + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).coin)); + return Background( child: SafeArea( child: Scaffold( @@ -77,26 +95,33 @@ class _OrdinalDetailsViewState extends State { height: _spacing, ), _DetailsItemWCopy( - title: "ID", + title: "Inscription ID", data: widget.ordinal.inscriptionId, ), + // const SizedBox( + // height: _spacing, + // ), + // // todo: add utxo status const SizedBox( height: _spacing, ), - // todo: add utxo status - const SizedBox( - height: _spacing, - ), - const _DetailsItemWCopy( + _DetailsItemWCopy( title: "Amount", - data: "TODO", // TODO infer from utxo utxoTXID:utxoVOUT + data: utxo == null + ? "ERROR" + : ref.watch(pAmountFormatter(coin)).format( + Amount( + rawValue: BigInt.from(utxo!.value), + fractionDigits: coin.decimals, + ), + ), ), const SizedBox( height: _spacing, ), - const _DetailsItemWCopy( + _DetailsItemWCopy( title: "Owner address", - data: "TODO", // infer from address associated w utxoTXID + data: utxo?.address ?? "ERROR", ), const SizedBox( height: _spacing, @@ -154,11 +179,27 @@ class _DetailsItemWCopy extends StatelessWidget { ); } }, - child: SvgPicture.asset( - Assets.svg.copy, - color: - Theme.of(context).extension()!.infoItemIcons, - width: 12, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + color: Theme.of(context) + .extension()! + .infoItemIcons, + width: 12, + ), + const SizedBox( + width: 6, + ), + Text( + "Copy", + style: STextStyles.infoSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemIcons, + ), + ), + ], ), ), ], From bce8ae6b4e99177e89b9d50c830d01215302b0f6 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 27 Jul 2023 10:53:40 -0600 Subject: [PATCH 7/7] update _currentDefaultThemeVersion to handle xlm asset updates to default themes --- lib/themes/theme_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/themes/theme_service.dart b/lib/themes/theme_service.dart index c9a8bd978..baeac44f2 100644 --- a/lib/themes/theme_service.dart +++ b/lib/themes/theme_service.dart @@ -27,7 +27,7 @@ final pThemeService = Provider((ref) { }); class ThemeService { - static const _currentDefaultThemeVersion = 3; + static const _currentDefaultThemeVersion = 4; ThemeService._(); static ThemeService? _instance; static ThemeService get instance => _instance ??= ThemeService._();