From 8ff9e3eca784e7845a5e21163e09b727588c6495 Mon Sep 17 00:00:00 2001 From: Lev Kropp Date: Tue, 14 Jan 2025 13:48:42 -0500 Subject: [PATCH] implement click-to-copy in vm_table_headers --- src/client/gui/lib/copyable_text.dart | 48 +++++++++++++++++++ .../gui/lib/vm_details/ip_addresses.dart | 14 ++++-- .../lib/vm_details/vm_details_general.dart | 46 +----------------- .../gui/lib/vm_table/vm_table_headers.dart | 14 ++++-- 4 files changed, 68 insertions(+), 54 deletions(-) create mode 100644 src/client/gui/lib/copyable_text.dart diff --git a/src/client/gui/lib/copyable_text.dart b/src/client/gui/lib/copyable_text.dart new file mode 100644 index 0000000000..5c70c17bf1 --- /dev/null +++ b/src/client/gui/lib/copyable_text.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart' hide Tooltip; +import 'package:flutter/services.dart'; +import 'tooltip.dart'; + +class CopyableText extends StatefulWidget { + final String text; + final TextStyle? style; + + const CopyableText(this.text, {super.key, this.style}); + + @override + State createState() => _CopyableTextState(); +} + +class _CopyableTextState extends State { + bool _copied = false; + + void _copyToClipboard() async { + await Clipboard.setData(ClipboardData(text: widget.text)); + setState(() => _copied = true); + } + + void _resetCopied() { + if (_copied) { + setState(() => _copied = false); + } + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onExit: (_) => _resetCopied(), + child: GestureDetector( + onTap: _copyToClipboard, + child: Tooltip( + message: _copied ? 'Copied' : 'Click to copy', + child: Text( + widget.text, + style: widget.style, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ); + } +} diff --git a/src/client/gui/lib/vm_details/ip_addresses.dart b/src/client/gui/lib/vm_details/ip_addresses.dart index 89829be826..fa8ac29d99 100644 --- a/src/client/gui/lib/vm_details/ip_addresses.dart +++ b/src/client/gui/lib/vm_details/ip_addresses.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart' hide Tooltip; +import '../copyable_text.dart'; import '../extensions.dart'; import '../tooltip.dart'; class IpAddresses extends StatelessWidget { final Iterable ips; + final bool copyable; - const IpAddresses(this.ips, {super.key}); + const IpAddresses(this.ips, {this.copyable = false, super.key}); @override Widget build(BuildContext context) { @@ -15,10 +17,12 @@ class IpAddresses extends StatelessWidget { return Row(children: [ Expanded( - child: Tooltip( - message: firstIp, - child: Text(firstIp.nonBreaking, overflow: TextOverflow.ellipsis), - ), + child: copyable + ? CopyableText(firstIp) + : Tooltip( + message: firstIp, + child: Text(firstIp.nonBreaking, overflow: TextOverflow.ellipsis), + ), ), if (restIps.isNotEmpty) Badge.count( diff --git a/src/client/gui/lib/vm_details/vm_details_general.dart b/src/client/gui/lib/vm_details/vm_details_general.dart index c5c1c1dc08..1bf03d47d9 100644 --- a/src/client/gui/lib/vm_details/vm_details_general.dart +++ b/src/client/gui/lib/vm_details/vm_details_general.dart @@ -12,51 +12,7 @@ import 'vm_action_buttons.dart'; import 'vm_details.dart'; import 'vm_status_icon.dart'; import '../tooltip.dart'; - -class CopyableText extends StatefulWidget { - final String text; - final TextStyle? style; - - const CopyableText(this.text, {super.key, this.style}); - - @override - State createState() => _CopyableTextState(); -} - -class _CopyableTextState extends State { - bool _copied = false; - - void _copyToClipboard() async { - await Clipboard.setData(ClipboardData(text: widget.text)); - setState(() => _copied = true); - } - - void _resetCopied() { - if (_copied) { - setState(() => _copied = false); - } - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - onExit: (_) => _resetCopied(), - child: GestureDetector( - onTap: _copyToClipboard, - child: Tooltip( - message: _copied ? 'Copied' : 'Click to copy', - child: Text( - widget.text, - style: widget.style, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ); - } -} +import '../copyable_text.dart'; class VmDetailsHeader extends ConsumerWidget { final String name; diff --git a/src/client/gui/lib/vm_table/vm_table_headers.dart b/src/client/gui/lib/vm_table/vm_table_headers.dart index 173d8348eb..d687a330c2 100644 --- a/src/client/gui/lib/vm_table/vm_table_headers.dart +++ b/src/client/gui/lib/vm_table/vm_table_headers.dart @@ -14,6 +14,7 @@ import '../vm_details/vm_status_icon.dart'; import 'search_box.dart'; import 'table.dart'; import 'vms.dart'; +import '../copyable_text.dart'; final headers = >[ TableHeader( @@ -72,9 +73,8 @@ final headers = >[ minWidth: 70, cellBuilder: (info) { final image = info.instanceInfo.currentRelease; - return Text( + return CopyableText( image.isNotBlank ? image.nonBreaking : '-', - overflow: TextOverflow.ellipsis, ); }, ), @@ -82,13 +82,19 @@ final headers = >[ name: 'PRIVATE IP', width: 140, minWidth: 100, - cellBuilder: (info) => IpAddresses(info.instanceInfo.ipv4.take(1)), + cellBuilder: (info) => IpAddresses( + info.instanceInfo.ipv4.take(1), + copyable: true, + ), ), TableHeader( name: 'PUBLIC IP', width: 140, minWidth: 100, - cellBuilder: (info) => IpAddresses(info.instanceInfo.ipv4.skip(1)), + cellBuilder: (info) => IpAddresses( + info.instanceInfo.ipv4.skip(1), + copyable: true, + ), ), ];