add copy indicator on tx details screen (#3152)

This commit is contained in:
malik1004x
2026-04-14 05:45:31 +02:00
committed by GitHub
parent dee2b09ebe
commit c338c6d6b8
6 changed files with 163 additions and 78 deletions
@@ -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;
@@ -99,10 +99,7 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
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?) ??
+59
View File
@@ -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<CopyWrapper> createState() => _CopyWrapperState();
}
class _CopyWrapperState extends State<CopyWrapper> {
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<bool> 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;
}
}
}
@@ -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,
@@ -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!
],
);
});
);
}),
),
);
}
}
@@ -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,