diff --git a/lib/widgets/reusable_widgets/context_menu_region.dart b/lib/widgets/reusable_widgets/context_menu_region.dart new file mode 100644 index 00000000..f5f7b305 --- /dev/null +++ b/lib/widgets/reusable_widgets/context_menu_region.dart @@ -0,0 +1,97 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +typedef ContextMenuBuilder = Widget Function( + BuildContext context, Offset offset); + +/// Shows and hides the context menu based on user gestures. +/// +/// By default, shows the menu on right clicks and long presses. +class ContextMenuRegion extends StatefulWidget { + /// Creates an instance of [ContextMenuRegion]. + const ContextMenuRegion({ + Key? key, + required this.child, + required this.contextMenuBuilder, + }) : super(key: key); + + /// Builds the context menu. + final ContextMenuBuilder contextMenuBuilder; + + /// The child widget that will be listened to for gestures. + final Widget child; + + @override + State createState() => _ContextMenuRegionState(); +} + +class _ContextMenuRegionState extends State { + Offset? _longPressOffset; + + final ContextMenuController _contextMenuController = ContextMenuController(); + + static bool get _longPressEnabled { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.iOS: + return true; + case TargetPlatform.macOS: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return false; + } + } + + void _onSecondaryTapUp(TapUpDetails details) { + _show(details.globalPosition); + } + + void _onTap() { + if (!_contextMenuController.isShown) { + return; + } + _hide(); + } + + void _onLongPressStart(LongPressStartDetails details) { + _longPressOffset = details.globalPosition; + } + + void _onLongPress() { + assert(_longPressOffset != null); + _show(_longPressOffset!); + _longPressOffset = null; + } + + void _show(Offset position) { + _contextMenuController.show( + context: context, + contextMenuBuilder: (context) { + return widget.contextMenuBuilder(context, position); + }, + ); + } + + void _hide() { + _contextMenuController.remove(); + } + + @override + void dispose() { + _hide(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onSecondaryTapUp: _onSecondaryTapUp, + onTap: _onTap, + onLongPress: _longPressEnabled ? _onLongPress : null, + onLongPressStart: _longPressEnabled ? _onLongPressStart : null, + child: widget.child, + ); + } +} diff --git a/lib/widgets/reusable_widgets/input_fields/input_field.dart b/lib/widgets/reusable_widgets/input_fields/input_field.dart index d40fc02c..f0f8c1b7 100644 --- a/lib/widgets/reusable_widgets/input_fields/input_field.dart +++ b/lib/widgets/reusable_widgets/input_fields/input_field.dart @@ -79,7 +79,7 @@ class _InputFieldState extends State { child: Text( AdaptiveTextSelectionToolbar.getButtonLabel( context, buttonItem), - style: const TextStyle(color: Colors.white)), + style: Theme.of(context).textTheme.bodyMedium), )) ]); }).toList()); diff --git a/lib/widgets/reusable_widgets/receive_qr_image.dart b/lib/widgets/reusable_widgets/receive_qr_image.dart index 4d3165d1..638985f2 100644 --- a/lib/widgets/reusable_widgets/receive_qr_image.dart +++ b/lib/widgets/reusable_widgets/receive_qr_image.dart @@ -1,6 +1,16 @@ +import 'dart:io'; +import 'dart:typed_data'; + import 'package:flutter/material.dart'; +import 'package:flutter_vector_icons/flutter_vector_icons.dart'; +import 'package:open_filex/open_filex.dart'; +import 'package:path/path.dart' as path; import 'package:pretty_qr_code/pretty_qr_code.dart'; +import 'package:screenshot/screenshot.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:zenon_syrius_wallet_flutter/utils/color_utils.dart'; +import 'package:zenon_syrius_wallet_flutter/utils/utils.dart'; +import 'package:zenon_syrius_wallet_flutter/widgets/reusable_widgets/context_menu_region.dart'; import 'package:znn_sdk_dart/znn_sdk_dart.dart'; class ReceiveQrImage extends StatelessWidget { @@ -9,7 +19,9 @@ class ReceiveQrImage extends StatelessWidget { final TokenStandard tokenStandard; final BuildContext context; - const ReceiveQrImage({ + final ScreenshotController screenshotController = ScreenshotController(); + + ReceiveQrImage({ required this.data, required this.size, required this.tokenStandard, @@ -19,25 +31,129 @@ class ReceiveQrImage extends StatelessWidget { @override Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular( - 15.0, - ), - child: Container( - padding: const EdgeInsets.all( - 10.0, - ), - color: Theme.of(context).colorScheme.background, - child: PrettyQr( - data: data, - size: size, - elementColor: ColorUtils.getTokenColor(tokenStandard), - image: const AssetImage('assets/images/qr_code_child_image_znn.png'), - typeNumber: 7, - errorCorrectLevel: QrErrorCorrectLevel.M, - roundEdges: true, - ), - ), - ); + return Screenshot( + controller: screenshotController, + child: ClipRRect( + borderRadius: BorderRadius.circular( + 15.0, + ), + child: Container( + padding: const EdgeInsets.all( + 10.0, + ), + color: Theme.of(context).colorScheme.background, + child: ContextMenuRegion( + contextMenuBuilder: (context, offset) { + return AdaptiveTextSelectionToolbar( + anchors: TextSelectionToolbarAnchors( + primaryAnchor: offset, + ), + children: [ + Row( + children: [ + Expanded( + child: Directionality( + textDirection: TextDirection.rtl, + child: TextButton.icon( + icon: Icon( + MaterialCommunityIcons.share, + color: + Theme.of(context).colorScheme.onBackground, + size: 14, + ), + onPressed: () { + ContextMenuController.removeAny(); + _shareQR(); + }, + style: TextButton.styleFrom( + shape: const RoundedRectangleBorder(), + ), + label: Text( + AdaptiveTextSelectionToolbar.getButtonLabel( + context, + ContextMenuButtonItem( + label: 'Share QR', onPressed: () {})), + style: + Theme.of(context).textTheme.bodyMedium), + ), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 1, + child: Directionality( + textDirection: TextDirection.rtl, + child: TextButton.icon( + icon: Icon( + Icons.save_alt, + color: + Theme.of(context).colorScheme.onBackground, + size: 14, + ), + onPressed: () { + ContextMenuController.removeAny(); + _saveQR(); + }, + style: TextButton.styleFrom( + shape: const RoundedRectangleBorder(), + ), + label: Text( + AdaptiveTextSelectionToolbar.getButtonLabel( + context, + ContextMenuButtonItem( + label: 'Save QR', onPressed: () {})), + style: + Theme.of(context).textTheme.bodyMedium), + ), + ), + ), + ], + ), + ], + ); + }, + child: PrettyQr( + data: data, + size: size, + elementColor: ColorUtils.getTokenColor(tokenStandard), + image: const AssetImage( + 'assets/images/qr_code_child_image_znn.png'), + typeNumber: 7, + errorCorrectLevel: QrErrorCorrectLevel.M, + roundEdges: true, + ), + ), + ), + )); + } + + void _saveQR() async { + Uint8List? capture = await screenshotController.capture( + delay: const Duration(milliseconds: 20)); + if (capture != null) { + String fileName = DateTime.now().millisecondsSinceEpoch.toString(); + final imagePath = await File( + '${znnDefaultPaths.cache.path}${path.separator}$fileName.png') + .create(); + await imagePath.writeAsBytes(capture); + await OpenFilex.open(imagePath.path); + } + } + + void _shareQR() async { + Uint8List? capture = await screenshotController.capture( + delay: const Duration(milliseconds: 20)); + if (capture != null) { + String fileName = DateTime.now().millisecondsSinceEpoch.toString(); + final imagePath = await File( + '${znnDefaultPaths.cache.path}${path.separator}$fileName.png') + .create(); + await imagePath.writeAsBytes(capture); + await Share.shareXFiles([XFile(imagePath.path)]); + } } } diff --git a/pubspec.yaml b/pubspec.yaml index 8b3e871e..bd90c07e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,10 +33,11 @@ dependencies: package_info_plus: ^1.3.0 device_info_plus: ^4.0.0 infinite_scroll_pagination: ^3.1.0 - share_plus: ^4.0.4 + share_plus: ^6.3.0 page_transition: ^2.0.4 file_selector_platform_interface: ^2.0.4 - pretty_qr_code: ^2.0.2 + pretty_qr_code: ^2.0.3 + screenshot: ^1.3.0 desktop_drop: ^0.4.0 network_info_plus: ^3.0.1 validators: ^3.0.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0a12ed49..03df1ac4 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("NetworkInfoPlusWindowsPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); TrayManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ecf4ed83..a12c21f5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST local_notifier network_info_plus screen_retriever + share_plus tray_manager url_launcher_windows window_manager