From c338c6d6b8c5bc0f64e2044e490b068a5f969d86 Mon Sep 17 00:00:00 2001 From: malik1004x Date: Tue, 14 Apr 2026 05:45:31 +0200 Subject: [PATCH] add copy indicator on tx details screen (#3152) --- .../list_item/list_item_regular_row.dart | 4 +- .../transaction_details_modal.dart | 5 +- lib/new-ui/widgets/copy_wrapper.dart | 59 ++++++ .../new_list_row/list_Item_style_wrapper.dart | 4 +- .../list_item_regular_row_widget.dart | 168 ++++++++++-------- .../new_list_row/new_list_section.dart | 1 + 6 files changed, 163 insertions(+), 78 deletions(-) create mode 100644 lib/new-ui/widgets/copy_wrapper.dart diff --git a/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart b/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart index a9ef260d0..f6df44a25 100644 --- a/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart +++ b/lib/entities/new_ui_entities/list_item/list_item_regular_row.dart @@ -15,13 +15,15 @@ class ListItemRegularRow extends ListItem { this.trailingWidget, this.truncateTrailingText = false, this.foregroundColor, - this.trailingIconSize + this.trailingIconSize, + this.copyableText }); final String? subtitle; final String? trailingText; final String? iconPath; final String? trailingIconPath; + final String? copyableText; final VoidCallback? onTap; final bool showArrow; final Widget? bottomWidget; diff --git a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart index a4b945154..f7e6c952f 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart @@ -99,10 +99,7 @@ class _TransactionDetailsModalState extends State { item.value.length > 25; return ListItemRegularRow( - onTap: () { - Clipboard.setData( - ClipboardData(text: item.value)); - }, + copyableText: item.value, showArrow: false, keyValue: ((item.key as ValueKey?)?.value as String?) ?? diff --git a/lib/new-ui/widgets/copy_wrapper.dart b/lib/new-ui/widgets/copy_wrapper.dart new file mode 100644 index 000000000..254d02f5d --- /dev/null +++ b/lib/new-ui/widgets/copy_wrapper.dart @@ -0,0 +1,59 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class CopyWrapper extends StatefulWidget { + const CopyWrapper( + {super.key, + this.data, + required this.builder, + this.duration = const Duration(milliseconds: 1200)}); + + final ClipboardData? data; + final Widget Function(BuildContext, bool) builder; + final Duration duration; + + @override + State createState() => _CopyWrapperState(); +} + +class _CopyWrapperState extends State { + bool copied = false; + + void handleCopy() async { + if (widget.data == null) return; + Clipboard.setData(widget.data!); + if (await shouldShowCopied()) { + setState(() => copied = true); + Future.delayed(widget.duration, () { + if (mounted) setState(() => copied = false); + }); + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: handleCopy, + child: widget.builder(context, copied), + ); + } + + // android 13 (sdk 33) added a built-in "text was copied to clipboard" ui element + Future shouldShowCopied() async { + if (!Platform.isAndroid) return true; + + try { + final deviceInfo = DeviceInfoPlugin(); + final androidInfo = await deviceInfo.androidInfo; + final sdk = androidInfo.version.sdkInt; + + return sdk < 33; + } catch (_) { + return true; + } + } +} diff --git a/lib/src/widgets/new_list_row/list_Item_style_wrapper.dart b/lib/src/widgets/new_list_row/list_Item_style_wrapper.dart index 9e5926cb3..d8a1186c5 100644 --- a/lib/src/widgets/new_list_row/list_Item_style_wrapper.dart +++ b/lib/src/widgets/new_list_row/list_Item_style_wrapper.dart @@ -6,6 +6,7 @@ class ListItemStyleWrapper extends StatelessWidget { required this.isFirstInSection, required this.isLastInSection, required this.builder, + this.backgroundColor, this.onTap, this.iconPath, this.height, @@ -16,6 +17,7 @@ class ListItemStyleWrapper extends StatelessWidget { final bool isLastInSection; final double? height; final VoidCallback? onTap; + final Color? backgroundColor; final Widget Function(BuildContext context, TextStyle textStyle, TextStyle labelStyle) builder; @override @@ -52,7 +54,7 @@ class ListItemStyleWrapper extends StatelessWidget { shape: RoundedSuperellipseBorder( borderRadius: radius, ), - color: theme.colorScheme.surfaceContainer, + color: backgroundColor ?? theme.colorScheme.surfaceContainer, ), child: Material( color: Colors.transparent, diff --git a/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart b/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart index 5343b8c38..47f5cc822 100644 --- a/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart +++ b/lib/src/widgets/new_list_row/list_item_regular_row_widget.dart @@ -1,7 +1,9 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; +import 'package:flutter/services.dart'; class ListItemRegularRowWidget extends StatelessWidget { const ListItemRegularRowWidget({ @@ -20,7 +22,8 @@ class ListItemRegularRowWidget extends StatelessWidget { this.foregroundColor, this.trailingIconSize, this.bottomWidget, - this.trailingWidget + this.trailingWidget, + this.copyableText }); final String keyValue; @@ -38,6 +41,7 @@ class ListItemRegularRowWidget extends StatelessWidget { final bool truncateTrailingText; final Color? foregroundColor; final double? trailingIconSize; + final String? copyableText; @override Widget build(BuildContext context) { @@ -47,82 +51,102 @@ class ListItemRegularRowWidget extends StatelessWidget { ? "${trailingText!.substring(0, 17)}..." : trailingText; - return ListItemStyleWrapper( - onTap: onTap, - iconPath: iconPath, - isFirstInSection: isFirstInSection, - isLastInSection: isLastInSection, - builder: (context, textStyle, labelStyle) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return CopyWrapper( + data: copyableText != null ? ClipboardData(text: copyableText!) : null, + builder: (context, copied) => AnimatedSwitcher( + duration: Duration(milliseconds: 150), + child: ListItemStyleWrapper( + key: ValueKey(copied), + backgroundColor: copied ? Theme.of(context).colorScheme.surfaceContainerHigh : null, + onTap: onTap, + iconPath: iconPath, + isFirstInSection: isFirstInSection, + isLastInSection: isLastInSection, + builder: (context, textStyle, labelStyle) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: Row( - children: [ - if (iconPath != null) - Padding( - padding: const EdgeInsets.only(right: 12.0), - child: CakeImageWidget(imageUrl: iconPath!, width: 24, height: 24,) - ), - Flexible( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, - style: foregroundColor == null - ? textStyle - : textStyle.copyWith(color: foregroundColor)), - if (subtitle != null) - Text( - subtitle!, - style: labelStyle.copyWith(fontSize: 12), - ), - ], - ), - ), - ], - ), - ), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (trailingTextToShow != null) - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text( - trailingTextToShow, - style: labelStyle, - ), - ), - if (trailingWidget != null) - trailingWidget! - else if (trailingIconPath != null) - CakeImageWidget(imageUrl: - trailingIconPath!, - height: trailingIconSize ?? 18, - width:trailingIconSize ?? 18, - colorFilter: ColorFilter.mode(foregroundColor ?? Theme.of(context).colorScheme.onSurfaceVariant,BlendMode.srcIn), - ) - else if (showArrow) - Padding( - padding: const EdgeInsets.symmetric(vertical: 7.0), - child: CakeImageWidget(imageUrl: - "assets/new-ui/arrow_forward.svg", - height: 14, - color: theme.colorScheme.onSurfaceVariant + Expanded( + child: Row( + children: [ + if (iconPath != null) + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: CakeImageWidget( + imageUrl: iconPath!, + width: 24, + height: 24, + )), + Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (copied) + Text( + S.of(context).copied, + style: textStyle.copyWith( + color: Theme.of(context).colorScheme.primary), + ) + else + Text(label, + style: foregroundColor == null + ? textStyle + : textStyle.copyWith(color: foregroundColor)), + if (subtitle != null) + Text( + subtitle!, + style: labelStyle.copyWith(fontSize: 12), + ), + ], + ), ), - ) + ], + ), + ), + Row( + children: [ + if (trailingTextToShow != null) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + trailingTextToShow, + style: labelStyle, + ), + ), + if (trailingWidget != null) + trailingWidget! + else if (trailingIconPath != null) + CakeImageWidget( + imageUrl: trailingIconPath!, + height: trailingIconSize ?? 18, + width: trailingIconSize ?? 18, + colorFilter: ColorFilter.mode( + foregroundColor ?? Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn), + ) + else if (showArrow) + Padding( + padding: const EdgeInsets.symmetric(vertical: 7.0), + child: CakeImageWidget(imageUrl: + "assets/new-ui/arrow_forward.svg", + height: 14, + color: theme.colorScheme.onSurfaceVariant + ), + ) + ], + ), ], ), + if (bottomWidget != null) bottomWidget! ], - ), - if (bottomWidget != null) bottomWidget! - ], - ); - }); + ); + }), + ), + ); } } diff --git a/lib/src/widgets/new_list_row/new_list_section.dart b/lib/src/widgets/new_list_row/new_list_section.dart index 21893d81e..196436124 100644 --- a/lib/src/widgets/new_list_row/new_list_section.dart +++ b/lib/src/widgets/new_list_row/new_list_section.dart @@ -103,6 +103,7 @@ class NewListSections extends StatelessWidget { showArrow: item.showArrow, truncateTrailingText: item.truncateTrailingText, foregroundColor: item.foregroundColor, + copyableText: item.copyableText, trailingIconSize: item.trailingIconSize, trailingWidget: item.trailingWidget, bottomWidget: item.bottomWidget,