From d541e586212a5e45ad339d87da6a1bac8a138cf8 Mon Sep 17 00:00:00 2001 From: OfficialDakari Date: Mon, 18 Aug 2025 17:06:16 +0500 Subject: [PATCH] make code blocks copyable --- lib/pages/chat/events/html_message.dart | 64 ++-- .../new_private_chat_view.dart | 333 +++++++++--------- lib/widgets/mxc_image.dart | 60 ++-- pubspec.lock | 20 +- pubspec.yaml | 1 + 5 files changed, 252 insertions(+), 226 deletions(-) diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart index cb8dcc1..a2cb56c 100644 --- a/lib/pages/chat/events/html_message.dart +++ b/lib/pages/chat/events/html_message.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:flutter_highlighter/flutter_highlighter.dart'; import 'package:flutter_highlighter/themes/shades-of-purple.dart'; +import 'package:highlight_selectable/theme_map.dart'; +import 'package:highlight_selectable/highlight_selectable.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:html/dom.dart' as dom; import 'package:html/parser.dart' as parser; @@ -272,37 +274,37 @@ class HtmlMessage extends StatelessWidget { ); case 'code': final isInline = node.parent?.localName != 'pre'; - return WidgetSpan( - child: Material( - clipBehavior: Clip.hardEdge, - borderRadius: BorderRadius.circular(4), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SelectableRegion( - selectionControls: MaterialTextSelectionControls(), - child: HighlightView( - node.text, - language: node.className - .split(' ') - .singleWhereOrNull( - (className) => className.startsWith('language-'), - ) - ?.split('language-') - .last ?? - 'md', - theme: shadesOfPurpleTheme, - padding: EdgeInsets.symmetric( - horizontal: 8, - vertical: isInline ? 0 : 8, - ), - textStyle: TextStyle( - fontSize: fontSize, - fontFamily: 'RobotoMono', - ), - )), - ), - ), - ); + return isInline + ? TextSpan( + text: node.text, + style: TextStyle( + fontSize: fontSize, + fontFamily: 'RobotoMono', + backgroundColor: const Color(0xff2d2b57), + color: const Color(0xffe3dfff), + ), + ) + : WidgetSpan( + child: HighlightSelectable( + node.text, + language: node.className + .split(' ') + .singleWhereOrNull( + (className) => className.startsWith('language-')) + ?.split('language-') + .last ?? + 'md', + theme: themeMap['shades-of-purple']!, + selectable: true, + showCopyButton: !isInline, + padding: EdgeInsets.symmetric( + horizontal: 8, + vertical: isInline ? 0 : 8, + ), + textStyle: + TextStyle(fontSize: fontSize, fontFamily: 'RobotoMono'), + ), + ); case 'img': final mxcUrl = Uri.tryParse(node.attributes['src'] ?? ''); if (mxcUrl == null || mxcUrl.scheme != 'mxc') { diff --git a/lib/pages/new_private_chat/new_private_chat_view.dart b/lib/pages/new_private_chat/new_private_chat_view.dart index d6ea35a..eac9113 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:extera_next/generated/l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:pretty_qr_code/pretty_qr_code.dart'; import 'package:extera_next/config/app_config.dart'; import 'package:extera_next/config/themes.dart'; +import 'package:extera_next/generated/l10n/l10n.dart'; import 'package:extera_next/pages/new_private_chat/new_private_chat.dart'; import 'package:extera_next/utils/localized_exception_extension.dart'; import 'package:extera_next/utils/platform_infos.dart'; @@ -99,176 +99,189 @@ class NewPrivateChatView extends StatelessWidget { ), ), Expanded( - child: AnimatedCrossFade( + child: AnimatedSwitcher( duration: FluffyThemes.animationDuration, - crossFadeState: searchResponse == null - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: ListView( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: SelectableText.rich( - TextSpan( - children: [ - TextSpan( - text: L10n.of(context).yourGlobalUserIdIs, - ), - TextSpan( - text: Matrix.of(context).client.userID, - style: const TextStyle( - fontWeight: FontWeight.w600, + child: searchResponse == null + ? ListView( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 18.0), + child: SelectableText.rich( + TextSpan( + children: [ + TextSpan( + text: L10n.of(context).yourGlobalUserIdIs, + ), + TextSpan( + text: Matrix.of(context).client.userID, + style: const TextStyle( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + style: TextStyle( + color: theme.colorScheme.onSurface, + fontSize: 12, ), ), - ], - ), - style: TextStyle( - color: theme.colorScheme.onSurface, - fontSize: 13, - ), - ), - ), - const SizedBox(height: 8), - ListTile( - leading: CircleAvatar( - backgroundColor: theme.colorScheme.secondaryContainer, - foregroundColor: theme.colorScheme.onSecondaryContainer, - child: Icon(Icons.adaptive.share_outlined), - ), - title: Text(L10n.of(context).shareInviteLink), - onTap: controller.inviteAction, - ), - ListTile( - leading: CircleAvatar( - backgroundColor: theme.colorScheme.tertiaryContainer, - foregroundColor: theme.colorScheme.onTertiaryContainer, - child: const Icon(Icons.group_add_outlined), - ), - title: Text(L10n.of(context).createGroup), - onTap: () => context.go('/rooms/newgroup'), - ), - if (PlatformInfos.isMobile) - ListTile( - leading: CircleAvatar( - backgroundColor: theme.colorScheme.primaryContainer, - foregroundColor: theme.colorScheme.onPrimaryContainer, - child: const Icon(Icons.qr_code_scanner_outlined), - ), - title: Text(L10n.of(context).scanQrCode), - onTap: controller.openScannerAction, - ), - Center( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 64.0, - vertical: 24.0, - ), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - color: theme.colorScheme.primaryContainer, - clipBehavior: Clip.hardEdge, - child: InkWell( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - onTap: () => showQrCodeViewer( - context, - userId, + ), + const SizedBox(height: 8), + ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.secondaryContainer, + foregroundColor: + theme.colorScheme.onSecondaryContainer, + child: Icon(Icons.adaptive.share_outlined), ), + title: Text(L10n.of(context).shareInviteLink), + onTap: controller.inviteAction, + ), + ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.tertiaryContainer, + foregroundColor: + theme.colorScheme.onTertiaryContainer, + child: const Icon(Icons.group_add_outlined), + ), + title: Text(L10n.of(context).createGroup), + onTap: () => context.go('/rooms/newgroup'), + ), + if (PlatformInfos.isMobile) + ListTile( + leading: CircleAvatar( + backgroundColor: + theme.colorScheme.primaryContainer, + foregroundColor: + theme.colorScheme.onPrimaryContainer, + child: + const Icon(Icons.qr_code_scanner_outlined), + ), + title: Text(L10n.of(context).scanQrCode), + onTap: controller.openScannerAction, + ), + Center( child: Padding( - padding: const EdgeInsets.all(32.0), - child: ConstrainedBox( - constraints: - const BoxConstraints(maxWidth: 256), - child: PrettyQrView.data( - data: 'https://matrix.to/#/$userId', - decoration: PrettyQrDecoration( - shape: PrettyQrSmoothSymbol( - roundFactor: 1, - color: - theme.colorScheme.onPrimaryContainer, + padding: const EdgeInsets.symmetric( + horizontal: 64.0, + vertical: 24.0, + ), + child: Material( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + side: BorderSide( + width: 3, + color: theme.colorScheme.primary, + ), + ), + color: Colors.transparent, + clipBehavior: Clip.hardEdge, + child: InkWell( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + onTap: () => showQrCodeViewer( + context, + userId, + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 200), + child: PrettyQrView.data( + data: 'https://matrix.to/#/$userId', + decoration: PrettyQrDecoration( + shape: PrettyQrSmoothSymbol( + roundFactor: 1, + color: theme.colorScheme.primary, + ), + ), + ), ), ), ), ), ), ), - ), + ], + ) + : FutureBuilder( + future: searchResponse, + builder: (context, snapshot) { + final result = snapshot.data; + final error = snapshot.error; + if (error != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + error.toLocalizedString(context), + textAlign: TextAlign.center, + style: TextStyle( + color: theme.colorScheme.error, + ), + ), + const SizedBox(height: 12), + OutlinedButton.icon( + onPressed: controller.searchUsers, + icon: const Icon(Icons.refresh_outlined), + label: Text(L10n.of(context).tryAgain), + ), + ], + ); + } + if (result == null) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } + if (result.isEmpty) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.search_outlined, size: 86), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + L10n.of(context).noUsersFoundWithQuery( + controller.controller.text, + ), + style: TextStyle( + color: theme.colorScheme.primary, + ), + textAlign: TextAlign.center, + ), + ), + ], + ); + } + return ListView.builder( + itemCount: result.length, + itemBuilder: (context, i) { + final contact = result[i]; + final displayname = contact.displayName ?? + contact.userId.localpart ?? + contact.userId; + return ListTile( + leading: Avatar( + name: displayname, + mxContent: contact.avatarUrl, + presenceUserId: contact.userId, + ), + title: Text(displayname), + subtitle: Text(contact.userId), + onTap: () => controller.openUserModal(contact), + ); + }, + ); + }, ), - ), - ], - ), - secondChild: FutureBuilder( - future: searchResponse, - builder: (context, snapshot) { - final result = snapshot.data; - final error = snapshot.error; - if (error != null) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - error.toLocalizedString(context), - textAlign: TextAlign.center, - style: TextStyle( - color: theme.colorScheme.error, - ), - ), - const SizedBox(height: 12), - OutlinedButton.icon( - onPressed: controller.searchUsers, - icon: const Icon(Icons.refresh_outlined), - label: Text(L10n.of(context).tryAgain), - ), - ], - ); - } - if (result == null) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } - if (result.isEmpty) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.search_outlined, size: 86), - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - L10n.of(context).noUsersFoundWithQuery( - controller.controller.text, - ), - style: TextStyle( - color: theme.colorScheme.primary, - ), - textAlign: TextAlign.center, - ), - ), - ], - ); - } - return ListView.builder( - itemCount: result.length, - itemBuilder: (context, i) { - final contact = result[i]; - final displayname = contact.displayName ?? - contact.userId.localpart ?? - contact.userId; - return ListTile( - leading: Avatar( - name: displayname, - mxContent: contact.avatarUrl, - presenceUserId: contact.userId, - ), - title: Text(displayname), - subtitle: Text(contact.userId), - onTap: () => controller.openUserModal(contact), - ); - }, - ); - }, - ), ), ), ], @@ -276,4 +289,4 @@ class NewPrivateChatView extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index 56a3565..b4aadc1 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -139,39 +139,33 @@ class _MxcImageState extends State { final data = _imageData; final hasData = data != null && data.isNotEmpty; - return AnimatedCrossFade( - crossFadeState: - hasData ? CrossFadeState.showSecond : CrossFadeState.showFirst, - duration: const Duration(milliseconds: 128), - firstChild: placeholder(context), - secondChild: hasData - ? Image.memory( - data, - width: widget.width, - height: widget.height, - fit: widget.fit, - filterQuality: - widget.isThumbnail ? FilterQuality.low : FilterQuality.medium, - errorBuilder: (context, e, s) { - Logs().d('Unable to render mxc image', e, s); - return SizedBox( - width: widget.width, - height: widget.height, - child: Material( - color: Theme.of(context).colorScheme.surfaceContainer, - child: Icon( - Icons.broken_image_outlined, - size: min(widget.height ?? 64, 64), - color: Theme.of(context).colorScheme.onSurface, + return AnimatedSwitcher( + duration: const Duration(milliseconds: 128), + child: hasData + ? Image.memory( + data, + width: widget.width, + height: widget.height, + fit: widget.fit, + filterQuality: widget.isThumbnail + ? FilterQuality.low + : FilterQuality.medium, + errorBuilder: (context, e, s) { + Logs().d('Unable to render mxc image', e, s); + return SizedBox( + width: widget.width, + height: widget.height, + child: Material( + color: Theme.of(context).colorScheme.surfaceContainer, + child: Icon( + Icons.broken_image_outlined, + size: min(widget.height ?? 64, 64), + color: Theme.of(context).colorScheme.onSurface, + ), ), - ), - ); - }, - ) - : SizedBox( - width: widget.width, - height: widget.height, - ), - ); + ); + }, + ) + : placeholder(context)); } } diff --git a/pubspec.lock b/pubspec.lock index 9640f34..028b4aa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -849,6 +849,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + highlight_selectable: + dependency: "direct main" + description: + name: highlight_selectable + sha256: c9a9c8741bd5ba2150f1ba2c713f4ea2d6dfd6b700d66ca11675882a75c4db20 + url: "https://pub.dev" + source: hosted + version: "0.1.4" highlighter: dependency: transitive description: @@ -1170,10 +1186,10 @@ packages: dependency: "direct main" description: name: matrix - sha256: "0c033a6ebf4ed2f56ed604769984072961fefc0cb255a802ed441dcaec490196" + sha256: "4e6c186115ee041c430aa5ed5210499d60e4323f907cea1f5e8a2f73a513a1bf" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a795c61..87ba628 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: desktop_notifications: ^0.6.3 device_info_plus: ^10.0.1 dynamic_color: ^1.8.1 + highlight_selectable: ^0.1.0 emoji_picker_flutter: ^3.1.0 emojis: ^0.9.9 #fcm_shared_isolate: ^0.2.0