import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui; import 'package:bluebubbles/app/layouts/conversation_list/widgets/tile/conversation_tile.dart'; import 'package:bluebubbles/helpers/helpers.dart'; import 'package:bluebubbles/models/models.dart'; import 'package:bluebubbles/services/services.dart'; import 'package:bluebubbles/utils/logger.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:image/image.dart' as img; import 'package:universal_io/io.dart'; class BackButton extends StatelessWidget { final bool Function()? onPressed; final Color? color; const BackButton({this.color, this.onPressed}); @override Widget build(BuildContext context) { return IconButton( icon: Obx(() => Icon( ss.settings.skin.value != Skins.Material ? CupertinoIcons.back : Icons.arrow_back, color: color ?? context.theme.colorScheme.primary, )), iconSize: ss.settings.skin.value != Skins.Material ? 30 : 24, onPressed: () { final result = onPressed?.call() ?? false; if (!result) { while (Get.isOverlaysOpen) { Get.back(); } Navigator.of(context).pop(); } }, ); } } Widget buildBackButton(BuildContext context, {EdgeInsets padding = EdgeInsets.zero, double? iconSize, Skins? skin, bool Function()? callback}) { return Material( color: Colors.transparent, child: Container( padding: padding, width: 25, child: IconButton( iconSize: iconSize ?? (ss.settings.skin.value != Skins.Material ? 30 : 24), icon: skin != null ? Icon(skin != Skins.Material ? CupertinoIcons.back : Icons.arrow_back, color: context.theme.colorScheme.primary) : Obx(() => Icon(ss.settings.skin.value != Skins.Material ? CupertinoIcons.back : Icons.arrow_back, color: context.theme.colorScheme.primary)), onPressed: () { final result = callback?.call() ?? true; if (result) { while (Get.isOverlaysOpen) { Get.back(); } Navigator.of(context).pop(); } }, ), ) ); } Widget buildProgressIndicator(BuildContext context, {double size = 20, double strokeWidth = 2}) { return ss.settings.skin.value == Skins.iOS ? Theme( data: ThemeData( cupertinoOverrideTheme: CupertinoThemeData(brightness: ThemeData.estimateBrightnessForColor(context.theme.colorScheme.background)), ), child: CupertinoActivityIndicator( radius: size / 2, ), ) : Container( constraints: BoxConstraints(maxHeight: size, maxWidth: size), child: CircularProgressIndicator( strokeWidth: strokeWidth, valueColor: AlwaysStoppedAnimation(context.theme.colorScheme.primary), )); } Widget buildImagePlaceholder(BuildContext context, Attachment attachment, Widget child, {bool isLoaded = false}) { double placeholderWidth = 200; double placeholderHeight = 150; // If the image doesn't have a valid size, show the loader with static height/width if (!attachment.hasValidSize) { return Container( width: placeholderWidth, height: placeholderHeight, color: context.theme.colorScheme.properSurface, child: child); } // If we have a valid size, we want to calculate the aspect ratio so the image doesn't "jitter" when loading // Calculate the aspect ratio for the placeholders double ratio = attachment.aspectRatio; double height = attachment.height?.toDouble() ?? placeholderHeight; double width = attachment.width?.toDouble() ?? placeholderWidth; // YES, this countainer surrounding the AspectRatio is needed. // If not there, the box may be too large return Container( constraints: BoxConstraints(maxHeight: height, maxWidth: width), child: AspectRatio( aspectRatio: ratio, child: Container(width: width, height: height, color: context.theme.colorScheme.properSurface, child: child))); } Future showConversationTileMenu(BuildContext context, ConversationTileController _this, Chat chat, Offset tapPosition, TextTheme textTheme) async { bool ios = ss.settings.skin.value == Skins.iOS; HapticFeedback.mediumImpact(); await showMenu( color: context.theme.colorScheme.properSurface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(ios ? 10 : 0)), context: context, position: RelativeRect.fromLTRB( tapPosition.dx, tapPosition.dy, tapPosition.dx, tapPosition.dy, ), items: [ if (!kIsWeb) PopupMenuItem( padding: EdgeInsets.zero, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { chat.togglePin(!chat.isPinned!); Navigator.pop(context); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 12.0), child: Row( children: [ Padding( padding: const EdgeInsets.only(right: 10), child: Icon( chat.isPinned! ? (ios ? CupertinoIcons.pin_slash : Icons.star_outline) : (ios ? CupertinoIcons.pin : Icons.star), color: context.theme.colorScheme.properOnSurface, ), ), Text( chat.isPinned! ? "Unpin" : "Pin", style: textTheme.bodyLarge!.copyWith(color: context.theme.colorScheme.properOnSurface), ), ], ), ), ), ), if (!kIsWeb) PopupMenuItem( padding: EdgeInsets.zero, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { chat.toggleMute(chat.muteType != "mute"); Navigator.pop(context); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 12.0), child: Row( children: [ Padding( padding: const EdgeInsets.only(right: 10), child: Icon( chat.muteType == "mute" ? (ios ? CupertinoIcons.bell : Icons.notifications_active) : (ios ? CupertinoIcons.bell_slash : Icons.notifications_off), color: context.theme.colorScheme.properOnSurface, ), ), Text(chat.muteType == "mute" ? 'Show Alerts' : 'Hide Alerts', style: textTheme.bodyLarge!.copyWith(color: context.theme.colorScheme.properOnSurface)), ], ), ), ), ), PopupMenuItem( padding: EdgeInsets.zero, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { chat.toggleHasUnread(!chat.hasUnreadMessage!); Navigator.pop(context); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 12.0), child: Row( children: [ Padding( padding: const EdgeInsets.only(right: 10), child: Icon( chat.hasUnreadMessage! ? (ios ? CupertinoIcons.person_crop_circle_badge_xmark : Icons.mark_chat_unread) : (ios ? CupertinoIcons.person_crop_circle_badge_checkmark : Icons.mark_chat_read), color: context.theme.colorScheme.properOnSurface, ), ), Text(chat.hasUnreadMessage! ? 'Mark Read' : 'Mark Unread', style: textTheme.bodyLarge!.copyWith(color: context.theme.colorScheme.properOnSurface)), ], ), ), ), ), if (!kIsWeb) PopupMenuItem( padding: EdgeInsets.zero, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { chat.toggleArchived(!chat.isArchived!); Navigator.pop(context); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 12.0), child: Row( children: [ Padding( padding: const EdgeInsets.only(right: 10), child: Icon( chat.isArchived! ? (ios ? CupertinoIcons.tray_arrow_up : Icons.unarchive) : (ios ? CupertinoIcons.tray_arrow_down : Icons.archive), color: context.theme.colorScheme.properOnSurface, ), ), Text( chat.isArchived! ? 'Unarchive' : 'Archive', style: textTheme.bodyLarge!.copyWith(color: context.theme.colorScheme.properOnSurface), ), ], ), ), ), ), if (!kIsWeb) PopupMenuItem( padding: EdgeInsets.zero, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () async { chats.removeChat(chat); Chat.softDelete(chat); Navigator.pop(context); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 12.0), child: Row( children: [ Padding( padding: const EdgeInsets.only(right: 10), child: Icon( Icons.delete_forever, color: context.theme.colorScheme.properOnSurface, ), ), Text( 'Delete', style: textTheme.bodyLarge!.copyWith(color: context.theme.colorScheme.properOnSurface), ), ], ), ), ), ), ], ); } IconData getAttachmentIcon(String mimeType) { if (mimeType.isEmpty) { return ss.settings.skin.value == Skins.iOS ? CupertinoIcons.arrow_up_right_square : Icons.open_in_new; } if (mimeType == "application/pdf") { return ss.settings.skin.value == Skins.iOS ? CupertinoIcons.doc_on_doc : Icons.picture_as_pdf; } else if (mimeType == "application/zip") { return ss.settings.skin.value == Skins.iOS ? CupertinoIcons.folder : Icons.folder; } else if (mimeType.startsWith("audio")) { return ss.settings.skin.value == Skins.iOS ? CupertinoIcons.music_note : Icons.music_note; } else if (mimeType.startsWith("image")) { return ss.settings.skin.value == Skins.iOS ? CupertinoIcons.photo : Icons.photo; } else if (mimeType.startsWith("video")) { return ss.settings.skin.value == Skins.iOS ? CupertinoIcons.videocam : Icons.videocam; } else if (mimeType.startsWith("text")) { return ss.settings.skin.value == Skins.iOS ? CupertinoIcons.doc_text : Icons.note; } return ss.settings.skin.value == Skins.iOS ? CupertinoIcons.arrow_up_right_square : Icons.open_in_new; } void showSnackbar(String title, String message, {int animationMs = 250, int durationMs = 1500, Function(GetBar)? onTap, TextButton? button}) { Get.snackbar(title, message, snackPosition: SnackPosition.BOTTOM, colorText: Get.theme.colorScheme.onInverseSurface, backgroundColor: Get.theme.colorScheme.inverseSurface, margin: const EdgeInsets.only(bottom: 10), maxWidth: Get.width - 20, isDismissible: false, duration: Duration(milliseconds: durationMs), animationDuration: Duration(milliseconds: animationMs), mainButton: button, onTap: onTap ?? (GetBar bar) { if (Get.isSnackbarOpen ?? false) Get.back(); }); } Widget getIndicatorIcon(SocketState socketState, {double size = 24, bool showAlpha = true}) { if (socketState == SocketState.connecting) { return Icon(Icons.fiber_manual_record, color: HexColor('ffd500').withAlpha(showAlpha ? 200 : 255), size: size); } else if (socketState == SocketState.connected) { return Icon(Icons.fiber_manual_record, color: HexColor('32CD32').withAlpha(showAlpha ? 200 : 255), size: size); } else { return Icon(Icons.fiber_manual_record, color: HexColor('DC143C').withAlpha(showAlpha ? 200 : 255), size: size); } } Color getIndicatorColor(SocketState socketState) { if (socketState == SocketState.connecting) { return HexColor('ffd500'); } else if (socketState == SocketState.connected) { return HexColor('32CD32'); } else { return HexColor('DC143C'); } } Future avatarAsBytes({ required Chat chat, List? participantsOverride, double quality = 256, }) async { final participants = participantsOverride ?? chat.participants; ui.PictureRecorder pictureRecorder = ui.PictureRecorder(); Canvas canvas = Canvas(pictureRecorder); await paintGroupAvatar(chat: chat, participants: participants, canvas: canvas, size: quality, usingParticipantsOverride: participantsOverride != null); ui.Picture picture = pictureRecorder.endRecording(); ui.Image image = await picture.toImage(quality.toInt(), quality.toInt()); Uint8List bytes = (await image.toByteData(format: ui.ImageByteFormat.png))!.buffer.asUint8List(); return bytes; } Future paintGroupAvatar({ required Chat chat, required List? participants, required Canvas canvas, required double size, required bool usingParticipantsOverride, }) async { late final ThemeData theme; final bool systemDark = SchedulerBinding.instance.window.platformBrightness == Brightness.dark; if (!ls.isAlive) { if (systemDark) { theme = ThemeStruct.getDarkTheme().data; } else { theme = ThemeStruct.getLightTheme().data; } } else { theme = Get.context!.theme; } if (chat.customAvatarPath != null && !usingParticipantsOverride) { Uint8List? customAvatar; try { customAvatar = await clip(await File(chat.customAvatarPath!).readAsBytes(), size: size.toInt(), circle: true); } catch (e) { Logger.error(e); } if (customAvatar != null) { canvas.drawImage(await loadImage(customAvatar), const Offset(0, 0), Paint()); return; } } if (participants == null) return; int maxAvatars = ss.settings.maxAvatarsInGroupWidget.value; if (participants.length == 1) { await paintAvatar( handle: participants.first, canvas: canvas, offset: const Offset(0, 0), size: size, ); return; } Color bgColor = theme.colorScheme.properSurface; if (kIsDesktop && systemDark && ss.settings.useWindowsAccent.value && Platform.isWindows) { bgColor = ts.windowsAccentColor ?? bgColor; } Paint paint = Paint()..color = bgColor; Offset _offset = Offset(size * 0.5, size * 0.5); if (kIsDesktop) { canvas.drawCircle(_offset, size * 0.5, paint); } else { canvas.drawRect(Rect.fromCenter(center: _offset, width: size, height: size), paint); } int realAvatarCount = min(participants.length, maxAvatars); for (int index = 0; index < realAvatarCount; index++) { double padding = size * 0.08; double angle = index / realAvatarCount * 2 * pi + pi * 0.25; double adjustedWidth = size * (-0.07 * realAvatarCount + 1); double innerRadius = size - adjustedWidth * 0.5 - 2 * padding; double realSize = adjustedWidth * 0.65; double top = size * 0.5 + (innerRadius * 0.5) * sin(angle + pi) - realSize * 0.5; double left = size * 0.5 - (innerRadius * 0.5) * cos(angle + pi) - realSize * 0.5; if (index == maxAvatars - 1 && participants.length > maxAvatars) { Paint paint = Paint(); paint.isAntiAlias = true; paint.color = theme.colorScheme.properSurface.withOpacity(0.8); Offset _offset = Offset(left + realSize * 0.5, top + realSize * 0.5); double radius = realSize * 0.5; canvas.drawCircle(_offset, radius, paint); IconData icon = Icons.people; TextPainter() ..textDirection = TextDirection.rtl ..textAlign = TextAlign.center ..text = TextSpan( text: String.fromCharCode(icon.codePoint), style: TextStyle( fontSize: adjustedWidth * 0.3, fontFamily: icon.fontFamily, color: theme.colorScheme.properOnSurface.withOpacity(0.8))) ..layout() ..paint(canvas, Offset(left + realSize * 0.25, top + realSize * 0.25)); } else { Paint paint = Paint()..color = ss.settings.skin.value == Skins.Samsung ? theme.colorScheme.secondary : theme.backgroundColor; canvas.drawCircle(Offset(left + realSize * 0.5, top + realSize * 0.5), realSize * 0.5, paint); await paintAvatar( handle: participants[index], canvas: canvas, offset: Offset(left + realSize * 0.01, top + realSize * 0.01), size: realSize * 0.99, borderWidth: size * 0.01, fontSize: adjustedWidth * 0.3, inGroup: true, ); } } } Future paintAvatar( {required Handle? handle, required Canvas canvas, required Offset offset, required double size, double? fontSize, double? borderWidth, bool inGroup=false}) async { fontSize ??= size * 0.5; borderWidth ??= size * 0.05; Contact? contact = handle?.contact ?? (handle != null ? cs.getContact(handle.address) : null); if (contact?.avatar != null) { Uint8List? contactAvatar = await clip(contact!.avatar ?? contact.avatar!, size: size.toInt(), circle: kIsDesktop || inGroup); if (contactAvatar != null) { canvas.drawImage(await loadImage(contactAvatar), offset, Paint()); return; } } List colors; if (handle?.color == null) { colors = toColorGradient(handle?.address); } else { colors = [ HexColor(handle!.color!).lightenAmount(0.02), HexColor(handle.color!), ]; } double dx = offset.dx; double dy = offset.dy; Paint paint = Paint(); paint.isAntiAlias = true; paint.shader = ui.Gradient.linear(Offset(dx + size * 0.5, dy + size * 0.5), Offset(size.toDouble(), size.toDouble()), [ !ss.settings.colorfulAvatars.value ? HexColor("928E8E") : colors.isNotEmpty ? colors[1] : HexColor("928E8E"), !ss.settings.colorfulAvatars.value ? HexColor("686868") : colors.isNotEmpty ? colors[0] : HexColor("686868"), ]); Offset _offset = Offset(dx + size * 0.5, dy + size * 0.5); double radius = size * 0.5; if (kIsDesktop || inGroup) { canvas.drawCircle(_offset, radius, paint); } else { canvas.drawRect(Rect.fromCenter(center: _offset, width: size, height: size), paint); } String? initials = handle == null ? "Y" : handle.initials; if (initials == null) { IconData icon = Icons.person; TextPainter() ..textDirection = TextDirection.rtl ..textAlign = TextAlign.center ..text = TextSpan( text: String.fromCharCode(icon.codePoint), style: TextStyle(fontSize: fontSize, fontFamily: icon.fontFamily)) ..layout() ..paint(canvas, Offset(dx + size * 0.25, dy + size * 0.25)); } else { TextPainter text = TextPainter() ..textDirection = TextDirection.ltr ..textAlign = TextAlign.center ..text = TextSpan( text: initials, style: TextStyle(fontSize: fontSize), ) ..layout(); text.paint(canvas, Offset(dx + (size - text.width) * 0.5, dy + (size - text.height) * 0.5)); } } Future clip(Uint8List data, {required int size, required bool circle}) async { ui.Image image; Uint8List _data = data; // Resize the image if it's the wrong size img.Image? _image = img.decodeImage(data); if (_image != null) { _image = img.copyResize(_image, width: size, height: size); _data = img.encodePng(_image) as Uint8List; } image = await loadImage(_data); ui.PictureRecorder pictureRecorder = ui.PictureRecorder(); Canvas canvas = Canvas(pictureRecorder); Paint paint = Paint(); paint.isAntiAlias = true; Rect bounds = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); Path path = circle ? (Path()..addOval(bounds)) : (Path()..addRect(bounds)); canvas.clipPath(path); canvas.drawImage(image, const Offset(0, 0), paint); ui.Picture picture = pictureRecorder.endRecording(); image = await picture.toImage(image.width, image.height); Uint8List? bytes = (await image.toByteData(format: ui.ImageByteFormat.png))?.buffer.asUint8List(); return bytes; } Future loadImage(Uint8List data) async { final Completer completer = Completer(); ui.decodeImageFromList(data, (ui.Image image) { return completer.complete(image); }); return completer.future; }