diff --git a/lib/pages/chat_details/chat_details_view.dart b/lib/pages/chat_details/chat_details_view.dart index 87baae98f..834b1a1f7 100644 --- a/lib/pages/chat_details/chat_details_view.dart +++ b/lib/pages/chat_details/chat_details_view.dart @@ -5,7 +5,6 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pages/chat_details/participant_list_item.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; @@ -15,6 +14,7 @@ import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/url_launcher.dart'; +import '../../widgets/qr_code_viewer.dart'; class ChatDetailsView extends StatelessWidget { final ChatDetailsController controller; @@ -60,10 +60,10 @@ class ChatDetailsView extends StatelessWidget { if (room.canonicalAlias.isNotEmpty) IconButton( tooltip: L10n.of(context).share, - icon: Icon(Icons.adaptive.share_outlined), - onPressed: () => FluffyShare.share( - AppConfig.inviteLinkPrefix + room.canonicalAlias, + icon: const Icon(Icons.qr_code_rounded), + onPressed: () => showQrCodeViewer( context, + room.canonicalAlias, ), ), if (controller.widget.embeddedCloseButton == null) 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 db18c61de..6dd01a13f 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import '../../widgets/qr_code_viewer.dart'; class NewPrivateChatView extends StatelessWidget { final NewPrivateChatController controller; @@ -25,6 +26,7 @@ class NewPrivateChatView extends StatelessWidget { final theme = Theme.of(context); final searchResponse = controller.searchResponse; + final userId = Matrix.of(context).client.userID!; return Scaffold( appBar: AppBar( scrolledUnderElevation: 0, @@ -157,26 +159,35 @@ class NewPrivateChatView extends StatelessWidget { ), Center( child: Padding( - padding: const EdgeInsets.all(64.0), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 256), - child: Material( - borderRadius: BorderRadius.circular(12), - elevation: 10, - color: Colors.white, - shadowColor: theme.appBarTheme.shadowColor, - clipBehavior: Clip.hardEdge, + 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, + ), child: Padding( - padding: const EdgeInsets.all(8), - child: PrettyQrView.data( - data: - 'https://matrix.to/#/${Matrix.of(context).client.userID}', - decoration: PrettyQrDecoration( - shape: PrettyQrSmoothSymbol( - roundFactor: 1, - color: theme.brightness == Brightness.light - ? theme.colorScheme.primary - : theme.colorScheme.onPrimary, + 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, + ), ), ), ), diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart index 6a5c09bae..17d899799 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/presence_builder.dart'; +import 'package:fluffychat/widgets/qr_code_viewer.dart'; import '../../widgets/matrix.dart'; import 'user_bottom_sheet.dart'; @@ -30,348 +31,340 @@ class UserBottomSheetView extends StatelessWidget { final client = Matrix.of(controller.widget.outerContext).client; final profileSearchError = controller.widget.profileSearchError; final dmRoomId = client.getDirectChatFromUserId(userId); - return SafeArea( - child: Scaffold( - appBar: AppBar( - leading: Center( - child: CloseButton( - onPressed: Navigator.of(context, rootNavigator: false).pop, - ), + return Scaffold( + appBar: AppBar( + leading: Center( + child: CloseButton( + onPressed: Navigator.of(context, rootNavigator: false).pop, ), - centerTitle: false, - title: Text(displayname), - actions: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: IconButton( - onPressed: () => FluffyShare.share( - 'https://matrix.to/#/$userId', - context, - ), - icon: Icon(Icons.adaptive.share_outlined), - ), - ), - ], ), - body: StreamBuilder( - stream: user?.room.client.onSync.stream.where( - (syncUpdate) => - syncUpdate.rooms?.join?[user.room.id]?.timeline?.events?.any( - (state) => state.type == EventTypes.RoomPowerLevels, - ) ?? - false, + centerTitle: false, + title: Text(displayname), + actions: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: IconButton( + onPressed: () => showQrCodeViewer(context, userId), + icon: const Icon(Icons.qr_code_outlined), + ), ), - builder: (context, snapshot) { - final theme = Theme.of(context); - return ListView( - children: [ - if (user?.membership == Membership.knock) - Padding( - padding: const EdgeInsets.all(12.0), - child: Material( - color: theme.colorScheme.surfaceContainerHigh, - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - child: ListTile( - minVerticalPadding: 16, - title: Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Text( - L10n.of(context) - .userWouldLikeToChangeTheChat(displayname), - ), - ), - subtitle: Row( - children: [ - TextButton.icon( - style: TextButton.styleFrom( - backgroundColor: theme.colorScheme.surface, - foregroundColor: theme.colorScheme.primary, - ), - onPressed: controller.knockAccept, - icon: const Icon(Icons.check_outlined), - label: Text(L10n.of(context).accept), - ), - const SizedBox(width: 12), - TextButton.icon( - style: TextButton.styleFrom( - backgroundColor: - theme.colorScheme.errorContainer, - foregroundColor: - theme.colorScheme.onErrorContainer, - ), - onPressed: controller.knockDecline, - icon: const Icon(Icons.cancel_outlined), - label: Text(L10n.of(context).decline), - ), - ], + ], + ), + body: StreamBuilder( + stream: user?.room.client.onSync.stream.where( + (syncUpdate) => + syncUpdate.rooms?.join?[user.room.id]?.timeline?.events?.any( + (state) => state.type == EventTypes.RoomPowerLevels, + ) ?? + false, + ), + builder: (context, snapshot) { + final theme = Theme.of(context); + return ListView( + children: [ + if (user?.membership == Membership.knock) + Padding( + padding: const EdgeInsets.all(12.0), + child: Material( + color: theme.colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + child: ListTile( + minVerticalPadding: 16, + title: Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text( + L10n.of(context) + .userWouldLikeToChangeTheChat(displayname), ), ), - ), - ), - Row( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Avatar( - client: - Matrix.of(controller.widget.outerContext).client, - mxContent: avatarUrl, - name: displayname, - size: Avatar.defaultSize * 2.5, - ), - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, + subtitle: Row( children: [ TextButton.icon( - onPressed: () => FluffyShare.share( - userId, - context, - copyOnly: true, - ), - icon: const Icon( - Icons.copy_outlined, - size: 14, - ), style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.onSurface, + backgroundColor: theme.colorScheme.surface, + foregroundColor: theme.colorScheme.primary, ), - label: Text( - userId, - maxLines: 1, - overflow: TextOverflow.ellipsis, + onPressed: controller.knockAccept, + icon: const Icon(Icons.check_outlined), + label: Text(L10n.of(context).accept), + ), + const SizedBox(width: 12), + TextButton.icon( + style: TextButton.styleFrom( + backgroundColor: theme.colorScheme.errorContainer, + foregroundColor: + theme.colorScheme.onErrorContainer, ), + onPressed: controller.knockDecline, + icon: const Icon(Icons.cancel_outlined), + label: Text(L10n.of(context).decline), + ), + ], + ), + ), + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Avatar( + client: Matrix.of(controller.widget.outerContext).client, + mxContent: avatarUrl, + name: displayname, + size: Avatar.defaultSize * 2.5, + ), + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton.icon( + onPressed: () => FluffyShare.share( + userId, + context, + copyOnly: true, + ), + icon: const Icon( + Icons.copy_outlined, + size: 14, + ), + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.onSurface, ), - PresenceBuilder( - userId: userId, - client: client, - builder: (context, presence) { - if (presence == null || - (presence.presence == PresenceType.offline && - presence.lastActiveTimestamp == null)) { - return const SizedBox.shrink(); - } + label: Text( + userId, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + PresenceBuilder( + userId: userId, + client: client, + builder: (context, presence) { + if (presence == null || + (presence.presence == PresenceType.offline && + presence.lastActiveTimestamp == null)) { + return const SizedBox.shrink(); + } - final dotColor = presence.presence.isOnline - ? Colors.green - : presence.presence.isUnavailable - ? Colors.orange - : Colors.grey; + final dotColor = presence.presence.isOnline + ? Colors.green + : presence.presence.isUnavailable + ? Colors.orange + : Colors.grey; - final lastActiveTimestamp = - presence.lastActiveTimestamp; + final lastActiveTimestamp = + presence.lastActiveTimestamp; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 16), - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: dotColor, - borderRadius: BorderRadius.circular(16), - ), + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 16), + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: dotColor, + borderRadius: BorderRadius.circular(16), ), - const SizedBox(width: 12), - if (presence.currentlyActive == true) - Text( - L10n.of(context).currentlyActive, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall, - ) - else if (lastActiveTimestamp != null) - Text( - L10n.of(context).lastActiveAgo( - lastActiveTimestamp - .localizedTimeShort(context), - ), - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall, + ), + const SizedBox(width: 12), + if (presence.currentlyActive == true) + Text( + L10n.of(context).currentlyActive, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall, + ) + else if (lastActiveTimestamp != null) + Text( + L10n.of(context).lastActiveAgo( + lastActiveTimestamp + .localizedTimeShort(context), ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - PresenceBuilder( - userId: userId, - client: client, - builder: (context, presence) { - final status = presence?.statusMsg; - if (status == null || status.isEmpty) { - return const SizedBox.shrink(); - } - return ListTile( - title: SelectableLinkify( - text: status, - style: const TextStyle(fontSize: 16), - options: const LinkifyOptions(humanize: false), - linkStyle: const TextStyle( - color: Colors.blueAccent, - decorationColor: Colors.blueAccent, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall, + ), + ], + ); + }, ), - onOpen: (url) => - UrlLauncher(context, url.url).launchUrl(), - ), - ); - }, - ), - if (userId != client.userID) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, + ], ), - child: ElevatedButton.icon( - onPressed: () => controller.participantAction( - UserBottomSheetAction.message, - ), - icon: const Icon(Icons.forum_outlined), - label: Text( - dmRoomId == null - ? L10n.of(context).startConversation - : L10n.of(context).sendAMessage, + ), + ], + ), + PresenceBuilder( + userId: userId, + client: client, + builder: (context, presence) { + final status = presence?.statusMsg; + if (status == null || status.isEmpty) { + return const SizedBox.shrink(); + } + return ListTile( + title: SelectableLinkify( + text: status, + style: const TextStyle(fontSize: 16), + options: const LinkifyOptions(humanize: false), + linkStyle: const TextStyle( + color: Colors.blueAccent, + decorationColor: Colors.blueAccent, ), + onOpen: (url) => + UrlLauncher(context, url.url).launchUrl(), ), + ); + }, + ), + if (userId != client.userID) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, ), - if (controller.widget.onMention != null) - ListTile( - leading: const Icon(Icons.alternate_email_outlined), - title: Text(L10n.of(context).mention), - onTap: () => controller - .participantAction(UserBottomSheetAction.mention), + child: ElevatedButton.icon( + onPressed: () => controller.participantAction( + UserBottomSheetAction.message, + ), + icon: const Icon(Icons.forum_outlined), + label: Text( + dmRoomId == null + ? L10n.of(context).startConversation + : L10n.of(context).sendAMessage, + ), ), - if (user != null) ...[ - Divider(color: theme.dividerColor), - ListTile( - title: Text(L10n.of(context).userRole), - leading: const Icon(Icons.admin_panel_settings_outlined), - trailing: Material( + ), + if (controller.widget.onMention != null) + ListTile( + leading: const Icon(Icons.alternate_email_outlined), + title: Text(L10n.of(context).mention), + onTap: () => controller + .participantAction(UserBottomSheetAction.mention), + ), + if (user != null) ...[ + Divider(color: theme.dividerColor), + ListTile( + title: Text(L10n.of(context).userRole), + leading: const Icon(Icons.admin_panel_settings_outlined), + trailing: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius / 2), + color: theme.colorScheme.onInverseSurface, + child: DropdownButton( + onChanged: user.canChangeUserPowerLevel || + // Workaround until https://github.com/famedly/matrix-dart-sdk/pull/1765 + (user.room.canChangePowerLevel && + user.id == user.room.client.userID) + ? controller.setPowerLevel + : null, + value: {0, 50, 100}.contains(user.powerLevel) + ? user.powerLevel + : null, + padding: const EdgeInsets.symmetric(horizontal: 8.0), borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), - color: theme.colorScheme.onInverseSurface, - child: DropdownButton( - onChanged: user.canChangeUserPowerLevel || - // Workaround until https://github.com/famedly/matrix-dart-sdk/pull/1765 - (user.room.canChangePowerLevel && - user.id == user.room.client.userID) - ? controller.setPowerLevel - : null, - value: {0, 50, 100}.contains(user.powerLevel) - ? user.powerLevel - : null, - padding: const EdgeInsets.symmetric(horizontal: 8.0), - borderRadius: - BorderRadius.circular(AppConfig.borderRadius / 2), - underline: const SizedBox.shrink(), - items: [ - DropdownMenuItem( - value: 0, - child: Text( - L10n.of(context).userLevel( - user.powerLevel < 50 ? user.powerLevel : 0, - ), + underline: const SizedBox.shrink(), + items: [ + DropdownMenuItem( + value: 0, + child: Text( + L10n.of(context).userLevel( + user.powerLevel < 50 ? user.powerLevel : 0, ), ), - DropdownMenuItem( - value: 50, - child: Text( - L10n.of(context).moderatorLevel( - user.powerLevel >= 50 && user.powerLevel < 100 - ? user.powerLevel - : 50, - ), + ), + DropdownMenuItem( + value: 50, + child: Text( + L10n.of(context).moderatorLevel( + user.powerLevel >= 50 && user.powerLevel < 100 + ? user.powerLevel + : 50, ), ), - DropdownMenuItem( - value: 100, - child: Text( - L10n.of(context).adminLevel( - user.powerLevel >= 100 ? user.powerLevel : 100, - ), + ), + DropdownMenuItem( + value: 100, + child: Text( + L10n.of(context).adminLevel( + user.powerLevel >= 100 ? user.powerLevel : 100, ), ), - DropdownMenuItem( - value: null, - child: Text(L10n.of(context).custom), - ), - ], - ), + ), + DropdownMenuItem( + value: null, + child: Text(L10n.of(context).custom), + ), + ], ), ), - ], - Divider(color: theme.dividerColor), - if (user != null && user.canKick) - ListTile( - textColor: theme.colorScheme.error, - iconColor: theme.colorScheme.error, - title: Text(L10n.of(context).kickFromChat), - leading: const Icon(Icons.exit_to_app_outlined), - onTap: () => controller - .participantAction(UserBottomSheetAction.kick), - ), - if (user != null && - user.canBan && - user.membership != Membership.ban) - ListTile( - textColor: theme.colorScheme.onErrorContainer, - iconColor: theme.colorScheme.onErrorContainer, - title: Text(L10n.of(context).banFromChat), - leading: const Icon(Icons.warning_sharp), - onTap: () => - controller.participantAction(UserBottomSheetAction.ban), - ) - else if (user != null && - user.canBan && - user.membership == Membership.ban) - ListTile( - title: Text(L10n.of(context).unbanFromChat), - leading: const Icon(Icons.warning_outlined), - onTap: () => controller - .participantAction(UserBottomSheetAction.unban), - ), - if (user != null && user.id != client.userID) - ListTile( - textColor: theme.colorScheme.onErrorContainer, - iconColor: theme.colorScheme.onErrorContainer, - title: Text(L10n.of(context).reportUser), - leading: const Icon(Icons.gavel_outlined), - onTap: () => controller - .participantAction(UserBottomSheetAction.report), - ), - if (profileSearchError != null) - ListTile( - leading: const Icon( - Icons.warning_outlined, - color: Colors.orange, - ), - subtitle: Text( - L10n.of(context).profileNotFound, - style: const TextStyle(color: Colors.orange), - ), + ), + ], + Divider(color: theme.dividerColor), + if (user != null && user.canKick) + ListTile( + textColor: theme.colorScheme.error, + iconColor: theme.colorScheme.error, + title: Text(L10n.of(context).kickFromChat), + leading: const Icon(Icons.exit_to_app_outlined), + onTap: () => + controller.participantAction(UserBottomSheetAction.kick), + ), + if (user != null && + user.canBan && + user.membership != Membership.ban) + ListTile( + textColor: theme.colorScheme.onErrorContainer, + iconColor: theme.colorScheme.onErrorContainer, + title: Text(L10n.of(context).banFromChat), + leading: const Icon(Icons.warning_sharp), + onTap: () => + controller.participantAction(UserBottomSheetAction.ban), + ) + else if (user != null && + user.canBan && + user.membership == Membership.ban) + ListTile( + title: Text(L10n.of(context).unbanFromChat), + leading: const Icon(Icons.warning_outlined), + onTap: () => + controller.participantAction(UserBottomSheetAction.unban), + ), + if (user != null && user.id != client.userID) + ListTile( + textColor: theme.colorScheme.onErrorContainer, + iconColor: theme.colorScheme.onErrorContainer, + title: Text(L10n.of(context).reportUser), + leading: const Icon(Icons.gavel_outlined), + onTap: () => controller + .participantAction(UserBottomSheetAction.report), + ), + if (profileSearchError != null) + ListTile( + leading: const Icon( + Icons.warning_outlined, + color: Colors.orange, ), - if (userId != client.userID && - !client.ignoredUsers.contains(userId)) - ListTile( - textColor: theme.colorScheme.onErrorContainer, - iconColor: theme.colorScheme.onErrorContainer, - leading: const Icon(Icons.block_outlined), - title: Text(L10n.of(context).block), - onTap: () => controller - .participantAction(UserBottomSheetAction.ignore), + subtitle: Text( + L10n.of(context).profileNotFound, + style: const TextStyle(color: Colors.orange), ), - ], - ); - }, - ), + ), + if (userId != client.userID && + !client.ignoredUsers.contains(userId)) + ListTile( + textColor: theme.colorScheme.onErrorContainer, + iconColor: theme.colorScheme.onErrorContainer, + leading: const Icon(Icons.block_outlined), + title: Text(L10n.of(context).block), + onTap: () => controller + .participantAction(UserBottomSheetAction.ignore), + ), + ], + ); + }, ), ); } diff --git a/lib/widgets/public_room_bottom_sheet.dart b/lib/widgets/public_room_bottom_sheet.dart index 48dc0e94f..ddf62a6bf 100644 --- a/lib/widgets/public_room_bottom_sheet.dart +++ b/lib/widgets/public_room_bottom_sheet.dart @@ -10,6 +10,7 @@ import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/qr_code_viewer.dart'; class PublicRoomBottomSheet extends StatelessWidget { final String? roomAlias; @@ -98,16 +99,17 @@ class PublicRoomBottomSheet extends StatelessWidget { ), ), actions: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: IconButton( - icon: Icon(Icons.adaptive.share_outlined), - onPressed: () => FluffyShare.share( - 'https://matrix.to/#/${roomAlias ?? chunk?.roomId}', - context, + if (roomAlias != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: IconButton( + icon: Icon(Icons.adaptive.share_outlined), + onPressed: () => showQrCodeViewer( + context, + roomAlias, + ), ), ), - ), ], ), body: FutureBuilder( diff --git a/lib/widgets/qr_code_viewer.dart b/lib/widgets/qr_code_viewer.dart new file mode 100644 index 000000000..41429f15e --- /dev/null +++ b/lib/widgets/qr_code_viewer.dart @@ -0,0 +1,138 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:image/image.dart'; +import 'package:matrix/matrix.dart'; +import 'package:pretty_qr_code/pretty_qr_code.dart'; +import 'package:qr_image/qr_image.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import '../config/themes.dart'; + +Future showQrCodeViewer( + BuildContext context, + String content, +) => + showDialog( + context: context, + builder: (context) => QrCodeViewer(content: content), + ); + +class QrCodeViewer extends StatelessWidget { + final String content; + + const QrCodeViewer({required this.content, super.key}); + + void _save(BuildContext context) async { + final imageResult = await showFutureLoadingDialog( + context: context, + future: () async { + final inviteLink = 'https://matrix.to/#/$content'; + final image = QRImage( + inviteLink, + size: 256, + radius: 1, + ).generate(); + return compute(encodePng, image); + }, + ); + final bytes = imageResult.result; + if (bytes == null) return; + if (!context.mounted) return; + + MatrixImageFile( + bytes: bytes, + name: 'QR_Code_$content.png', + mimeType: 'image/png', + ).save(context); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final inviteLink = 'https://matrix.to/#/$content'; + return Scaffold( + backgroundColor: Colors.black.withOpacity(0.5), + extendBodyBehindAppBar: true, + appBar: AppBar( + elevation: 0, + leading: IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withOpacity(0.5), + ), + icon: const Icon(Icons.close), + onPressed: Navigator.of(context).pop, + color: Colors.white, + tooltip: L10n.of(context).close, + ), + backgroundColor: Colors.transparent, + actions: [ + IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withOpacity(0.5), + ), + icon: Icon(Icons.adaptive.share_outlined), + onPressed: () => FluffyShare.share( + inviteLink, + context, + ), + color: Colors.white, + tooltip: L10n.of(context).share, + ), + const SizedBox(width: 8), + IconButton( + style: IconButton.styleFrom( + backgroundColor: Colors.black.withOpacity(0.5), + ), + icon: const Icon(Icons.download_outlined), + onPressed: () => _save(context), + color: Colors.white, + tooltip: L10n.of(context).downloadFile, + ), + const SizedBox(width: 8), + ], + ), + body: Center( + child: Container( + margin: const EdgeInsets.all(32.0), + padding: const EdgeInsets.all(32.0), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: FluffyThemes.columnWidth), + child: PrettyQrView.data( + data: inviteLink, + decoration: PrettyQrDecoration( + shape: PrettyQrSmoothSymbol( + roundFactor: 1, + color: theme.colorScheme.onPrimaryContainer, + ), + ), + ), + ), + const SizedBox(height: 8.0), + SelectableText( + content, + textAlign: TextAlign.center, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontSize: 12, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index cdd398ede..243d44073 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1554,6 +1554,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + qr_image: + dependency: "direct main" + description: + name: qr_image + sha256: c3cd2ac2c6cd6b14604c97b45c477b18988b6518f72120fa04418fc54e3b0d76 + url: "https://pub.dev" + source: hosted + version: "1.0.0" quiver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ac2dcb801..06b5ce67a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,6 +76,7 @@ dependencies: provider: ^6.0.2 punycode: ^1.0.0 qr_code_scanner: ^1.0.1 + qr_image: ^1.0.0 receive_sharing_intent: ^1.8.1 record: ^5.1.2 scroll_to_index: ^3.0.1