diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index a711070e0..75f83ab09 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -248,14 +248,12 @@ class MessageContent extends StatelessWidget { linkColor: linkColor, ); case MessageTypes.Video: - // #Pangea - // return EventVideoPlayer(event, textColor: textColor); return EventVideoPlayer( event, textColor: textColor, - chatController: controller, + linkColor: linkColor, + timeline: timeline, ); - // Pangea# case MessageTypes.File: return MessageDownloadContent( event, diff --git a/lib/pages/chat/events/video_player.dart b/lib/pages/chat/events/video_player.dart index b3b5bfc05..856bda50c 100644 --- a/lib/pages/chat/events/video_player.dart +++ b/lib/pages/chat/events/video_player.dart @@ -1,160 +1,51 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:chewie/chewie.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:matrix/matrix.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:universal_html/html.dart' as html; -import 'package:video_player/video_player.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pages/chat/events/image_bubble.dart'; import 'package:fluffychat/utils/file_description.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/blur_hash.dart'; -import '../../../utils/error_reporter.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; +import '../../image_viewer/image_viewer.dart'; -class EventVideoPlayer extends StatefulWidget { +class EventVideoPlayer extends StatelessWidget { final Event event; + final Timeline? timeline; final Color? textColor; final Color? linkColor; - // #Pangea - final ChatController? chatController; - // Pangea# const EventVideoPlayer( this.event, { + this.timeline, this.textColor, this.linkColor, - // #Pangea - this.chatController, - // Pangea# super.key, }); - @override - EventVideoPlayerState createState() => EventVideoPlayerState(); -} - -class EventVideoPlayerState extends State { - ChewieController? _chewieController; - VideoPlayerController? _videoPlayerController; - bool _isDownloading = false; - - // The video_player package only doesn't support Windows and Linux. - // #Pangea - // final _supportsVideoPlayer = - // !PlatformInfos.isWindows && !PlatformInfos.isLinux; - final _supportsVideoPlayer = !PlatformInfos.isLinux; - - StreamSubscription? _stopVideoSubscription; - // Pangea# - - void _downloadAction() async { - if (!_supportsVideoPlayer) { - widget.event.saveFile(context); - return; - } - - setState(() => _isDownloading = true); - - try { - final videoFile = await widget.event.downloadAndDecryptAttachment(); - - // Dispose the controllers if we already have them. - _disposeControllers(); - late VideoPlayerController videoPlayerController; - - // Create the VideoPlayerController from the contents of videoFile. - if (kIsWeb) { - final blob = html.Blob([videoFile.bytes]); - final networkUri = Uri.parse(html.Url.createObjectUrlFromBlob(blob)); - videoPlayerController = VideoPlayerController.networkUrl(networkUri); - } else { - final tempDir = await getTemporaryDirectory(); - final fileName = Uri.encodeComponent( - widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last, - ); - final file = File('${tempDir.path}/${fileName}_${videoFile.name}'); - if (await file.exists() == false) { - await file.writeAsBytes(videoFile.bytes); - } - videoPlayerController = VideoPlayerController.file(file); - } - _videoPlayerController = videoPlayerController; - - await videoPlayerController.initialize(); - - // Create a ChewieController on top. - _chewieController = ChewieController( - videoPlayerController: videoPlayerController, - useRootNavigator: !kIsWeb, - autoPlay: true, - autoInitialize: true, - ); - // #Pangea - _stopVideoSubscription?.cancel(); - _stopVideoSubscription = - widget.chatController?.stopMediaStream.stream.listen((_) { - _videoPlayerController?.pause(); - _chewieController?.pause(); - }); - // Pangea# - } on IOException catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(e.toLocalizedString(context)), - ), - ); - } catch (e, s) { - ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s); - } finally { - setState(() => _isDownloading = false); - } - } - - void _disposeControllers() { - _chewieController?.dispose(); - _videoPlayerController?.dispose(); - _chewieController = null; - _videoPlayerController = null; - // #Pangea - _stopVideoSubscription?.cancel(); - // Pangea# - } - - @override - void dispose() { - _disposeControllers(); - super.dispose(); - } - static const String fallbackBlurHash = 'L5H2EC=PM+yV0g-mq.wG9c010J}I'; @override Widget build(BuildContext context) { - final theme = Theme.of(context); + final supportsVideoPlayer = PlatformInfos.supportsVideoPlayer; - final hasThumbnail = widget.event.hasThumbnail; - final blurHash = (widget.event.infoMap as Map) + final blurHash = (event.infoMap as Map) .tryGet('xyz.amorgan.blurhash') ?? fallbackBlurHash; - final fileDescription = widget.event.fileDescription; - final textColor = widget.textColor; - final linkColor = widget.linkColor; + final fileDescription = event.fileDescription; + final infoMap = event.content.tryGetMap('info'); + final videoWidth = infoMap?.tryGet('w') ?? 400; + final videoHeight = infoMap?.tryGet('h') ?? 300; + const height = 300.0; + final width = videoWidth * (height / videoHeight); - const width = 300.0; + final durationInt = infoMap?.tryGet('duration'); + final duration = + durationInt == null ? null : Duration(milliseconds: durationInt); - final chewieController = _chewieController; return Column( mainAxisSize: MainAxisSize.min, spacing: 8, @@ -162,52 +53,69 @@ class EventVideoPlayerState extends State { Material( color: Colors.black, borderRadius: BorderRadius.circular(AppConfig.borderRadius), - child: SizedBox( - height: width, - child: chewieController != null - ? Center(child: Chewie(controller: chewieController)) - : Stack( - children: [ - if (hasThumbnail) - Center( - child: ImageBubble( - widget.event, - tapToView: false, - textColor: widget.textColor, - ), - ) - else - BlurHash( + child: InkWell( + onTap: () => supportsVideoPlayer + ? showDialog( + context: context, + builder: (_) => ImageViewer( + event, + timeline: timeline, + outerContext: context, + ), + ) + : event.saveFile(context), + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + child: SizedBox( + width: width, + height: height, + child: Hero( + tag: event.eventId, + child: Stack( + children: [ + if (event.hasThumbnail) + MxcImage( + event: event, + isThumbnail: true, + width: width, + height: height, + fit: BoxFit.cover, + placeholder: (context) => BlurHash( blurhash: blurHash, width: width, - height: width, + height: height, + fit: BoxFit.cover, ), - Center( - child: IconButton( - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.surface, + ) + else + BlurHash( + blurhash: blurHash, + width: width, + height: height, + fit: BoxFit.cover, + ), + Center( + child: CircleAvatar( + child: supportsVideoPlayer + ? const Icon(Icons.play_arrow_outlined) + : const Icon(Icons.file_download_outlined), + ), + ), + if (duration != null) + Positioned( + bottom: 8, + left: 16, + child: Text( + '${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}', + style: TextStyle( + color: Colors.white, + backgroundColor: Colors.black.withAlpha(32), ), - icon: _isDownloading - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ) - : _supportsVideoPlayer - ? const Icon(Icons.play_circle_outlined) - : const Icon(Icons.file_download_outlined), - tooltip: _isDownloading - ? L10n.of(context).loadingPleaseWait - : L10n.of(context).videoWithSize( - widget.event.sizeString ?? '?MB', - ), - onPressed: _isDownloading ? null : _downloadAction, ), ), - ], - ), + ], + ), + ), + ), ), ), if (fileDescription != null && textColor != null && linkColor != null) diff --git a/lib/pages/chat_list/client_chooser_button.dart b/lib/pages/chat_list/client_chooser_button.dart index 68acc13cb..717837320 100644 --- a/lib/pages/chat_list/client_chooser_button.dart +++ b/lib/pages/chat_list/client_chooser_button.dart @@ -4,7 +4,6 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -67,17 +66,16 @@ class ClientChooserButton extends StatelessWidget { ], ), ), - if (!FluffyThemes.isColumnMode(context)) - PopupMenuItem( - value: SettingsAction.settings, - child: Row( - children: [ - const Icon(Icons.settings_outlined), - const SizedBox(width: 18), - Text(L10n.of(context).settings), - ], - ), + PopupMenuItem( + value: SettingsAction.settings, + child: Row( + children: [ + const Icon(Icons.settings_outlined), + const SizedBox(width: 18), + Text(L10n.of(context).settings), + ], ), + ), const PopupMenuDivider(), for (final bundle in bundles) ...[ if (matrix.accountBundles[bundle]!.length != 1 || diff --git a/lib/pages/image_viewer/image_viewer.dart b/lib/pages/image_viewer/image_viewer.dart index 80f9b371d..d42a845a7 100644 --- a/lib/pages/image_viewer/image_viewer.dart +++ b/lib/pages/image_viewer/image_viewer.dart @@ -33,7 +33,13 @@ class ImageViewerController extends State { void initState() { super.initState(); allEvents = widget.timeline?.events - .where((event) => event.messageType == MessageTypes.Image) + .where( + (event) => { + MessageTypes.Image, + MessageTypes.Sticker, + if (PlatformInfos.supportsVideoPlayer) MessageTypes.Video, + }.contains(event.messageType), + ) .toList() .reversed .toList() ?? diff --git a/lib/pages/image_viewer/image_viewer_view.dart b/lib/pages/image_viewer/image_viewer_view.dart index 997a9e6f9..983171be2 100644 --- a/lib/pages/image_viewer/image_viewer_view.dart +++ b/lib/pages/image_viewer/image_viewer_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:fluffychat/pages/image_viewer/video_player.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; @@ -75,27 +77,46 @@ class ImageViewerView extends StatelessWidget { child: PageView.builder( controller: controller.pageController, itemCount: controller.allEvents.length, - itemBuilder: (context, i) => InteractiveViewer( - minScale: 1.0, - maxScale: 10.0, - onInteractionEnd: controller.onInteractionEnds, - child: Center( - child: Hero( - tag: controller.allEvents[i].eventId, - child: GestureDetector( - // Ignore taps to not go back here: - onTap: () {}, - child: MxcImage( - key: ValueKey(controller.allEvents[i].eventId), - event: controller.allEvents[i], - fit: BoxFit.contain, - isThumbnail: false, - animated: true, + itemBuilder: (context, i) { + final event = controller.allEvents[i]; + switch (event.messageType) { + case MessageTypes.Video: + return Padding( + padding: const EdgeInsets.only(top: 52.0), + child: Center( + child: GestureDetector( + // Ignore taps to not go back here: + onTap: () {}, + child: EventVideoPlayer(event), + ), ), - ), - ), - ), - ), + ); + case MessageTypes.Image: + case MessageTypes.Sticker: + default: + return InteractiveViewer( + minScale: 1.0, + maxScale: 10.0, + onInteractionEnd: controller.onInteractionEnds, + child: Center( + child: Hero( + tag: event.eventId, + child: GestureDetector( + // Ignore taps to not go back here: + onTap: () {}, + child: MxcImage( + key: ValueKey(event.eventId), + event: event, + fit: BoxFit.contain, + isThumbnail: false, + animated: true, + ), + ), + ), + ), + ); + } + }, ), ), if (hovered && controller.canGoBack) diff --git a/lib/pages/image_viewer/video_player.dart b/lib/pages/image_viewer/video_player.dart new file mode 100644 index 000000000..b85d6799d --- /dev/null +++ b/lib/pages/image_viewer/video_player.dart @@ -0,0 +1,167 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:chewie/chewie.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:video_player/video_player.dart'; + +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/blur_hash.dart'; +import '../../../utils/error_reporter.dart'; +import '../../widgets/mxc_image.dart'; + +class EventVideoPlayer extends StatefulWidget { + final Event event; + + const EventVideoPlayer( + this.event, { + super.key, + }); + + @override + EventVideoPlayerState createState() => EventVideoPlayerState(); +} + +class EventVideoPlayerState extends State { + ChewieController? _chewieController; + VideoPlayerController? _videoPlayerController; + + // The video_player package only doesn't support Windows and Linux. + final _supportsVideoPlayer = + !PlatformInfos.isWindows && !PlatformInfos.isLinux; + + void _downloadAction() async { + if (!_supportsVideoPlayer) { + widget.event.saveFile(context); + return; + } + + try { + final videoFile = await widget.event.downloadAndDecryptAttachment(); + + // Dispose the controllers if we already have them. + _disposeControllers(); + late VideoPlayerController videoPlayerController; + + // Create the VideoPlayerController from the contents of videoFile. + if (kIsWeb) { + final blob = html.Blob([videoFile.bytes]); + final networkUri = Uri.parse(html.Url.createObjectUrlFromBlob(blob)); + videoPlayerController = VideoPlayerController.networkUrl(networkUri); + } else { + final tempDir = await getTemporaryDirectory(); + final fileName = Uri.encodeComponent( + widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last, + ); + final file = File('${tempDir.path}/${fileName}_${videoFile.name}'); + if (await file.exists() == false) { + await file.writeAsBytes(videoFile.bytes); + } + videoPlayerController = VideoPlayerController.file(file); + } + _videoPlayerController = videoPlayerController; + + await videoPlayerController.initialize(); + + // Create a ChewieController on top. + setState(() { + _chewieController = ChewieController( + videoPlayerController: videoPlayerController, + showControlsOnInitialize: false, + autoPlay: true, + autoInitialize: true, + looping: true, + ); + }); + } on IOException catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(e.toLocalizedString(context)), + ), + ); + } catch (e, s) { + ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s); + } + } + + void _disposeControllers() { + _chewieController?.dispose(); + _videoPlayerController?.dispose(); + _chewieController = null; + _videoPlayerController = null; + } + + @override + void dispose() { + _disposeControllers(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _downloadAction(); + }); + } + + static const String fallbackBlurHash = 'L5H2EC=PM+yV0g-mq.wG9c010J}I'; + + @override + Widget build(BuildContext context) { + final hasThumbnail = widget.event.hasThumbnail; + final blurHash = (widget.event.infoMap as Map) + .tryGet('xyz.amorgan.blurhash') ?? + fallbackBlurHash; + final infoMap = widget.event.content.tryGetMap('info'); + final videoWidth = infoMap?.tryGet('w') ?? 400; + final videoHeight = infoMap?.tryGet('h') ?? 300; + final height = MediaQuery.of(context).size.height - 52; + final width = videoWidth * (height / videoHeight); + + final chewieController = _chewieController; + return chewieController != null + ? Center( + child: SizedBox( + width: width, + height: height, + child: Chewie(controller: chewieController), + ), + ) + : Stack( + children: [ + Center( + child: Hero( + tag: widget.event.eventId, + child: hasThumbnail + ? MxcImage( + event: widget.event, + isThumbnail: true, + width: width, + height: height, + fit: BoxFit.cover, + placeholder: (context) => BlurHash( + blurhash: blurHash, + width: width, + height: height, + fit: BoxFit.cover, + ), + ) + : BlurHash( + blurhash: blurHash, + width: width, + height: height, + ), + ), + ), + const Center(child: CircularProgressIndicator.adaptive()), + ], + ); + } +} diff --git a/lib/utils/file_selector.dart b/lib/utils/file_selector.dart index de23cd369..f9507355a 100644 --- a/lib/utils/file_selector.dart +++ b/lib/utils/file_selector.dart @@ -30,17 +30,11 @@ Future> selectFiles( if (allowMultiple) { return await AppLock.of(context).pauseWhile( - openFiles( - confirmButtonText: title, - acceptedTypeGroups: type.groups, - ), + openFiles(confirmButtonText: title, acceptedTypeGroups: type.groups), ); } final file = await AppLock.of(context).pauseWhile( - openFile( - confirmButtonText: title, - acceptedTypeGroups: type.groups, - ), + openFile(confirmButtonText: title, acceptedTypeGroups: type.groups), ); if (file == null) return []; return [file]; @@ -50,38 +44,46 @@ enum FileSelectorType { any([], FileType.any, null), images( [ + XTypeGroup( + label: 'Images', + extensions: [ + 'jpg', + 'JPG', + 'jpeg', + 'JPEG', + 'png', + 'PNG', + 'webp', + 'WebP', + 'WEBP', + 'gif', + 'GIF', + 'bmp', + 'BMP', + 'tiff', + 'TIFF', + 'tif', + 'TIF', + 'heic', + 'HEIC', + 'svg', + 'SVG', + ], + ), XTypeGroup( label: 'JPG', extensions: ['jpg', 'JPG', 'jpeg', 'JPEG'], ), - XTypeGroup( - label: 'PNGs', - extensions: ['png', 'PNG'], - ), - XTypeGroup( - label: 'WEBP', - extensions: ['WebP', 'WEBP'], - ), - XTypeGroup( - label: 'GIF', - extensions: ['gif', 'GIF'], - ), - XTypeGroup( - label: 'BMP', - extensions: ['bmp', 'BMP'], - ), + XTypeGroup(label: 'PNG', extensions: ['png', 'PNG']), + XTypeGroup(label: 'WebP', extensions: ['webp', 'WebP', 'WEBP']), + XTypeGroup(label: 'GIF', extensions: ['gif', 'GIF']), + XTypeGroup(label: 'BMP', extensions: ['bmp', 'BMP']), XTypeGroup( label: 'TIFF', extensions: ['tiff', 'TIFF', 'tif', 'TIF'], ), - XTypeGroup( - label: 'HEIC', - extensions: ['heic', 'HEIC'], - ), - XTypeGroup( - label: 'SVG', - extensions: ['svg', 'SVG'], - ), + XTypeGroup(label: 'HEIC', extensions: ['heic', 'HEIC']), + XTypeGroup(label: 'SVG', extensions: ['svg', 'SVG']), ], FileType.image, null, @@ -89,51 +91,48 @@ enum FileSelectorType { videos( [ XTypeGroup( - label: 'MP4', - extensions: ['mp4', 'MP4'], - ), - XTypeGroup( - label: 'AVI', - extensions: ['avi', 'AVI'], - ), - XTypeGroup( - label: 'MOV', - extensions: ['mov', 'MOV'], - ), - XTypeGroup( - label: 'MKV', - extensions: ['mkv', 'MKV'], - ), - XTypeGroup( - label: 'WMV', - extensions: ['wmv', 'WMV'], - ), - XTypeGroup( - label: 'FLV', - extensions: ['flv', 'FLV'], - ), - XTypeGroup( - label: 'MPEG', - extensions: ['mpeg', 'MPEG'], - ), - XTypeGroup( - label: '3GP', - extensions: ['3gp', '3GP'], - ), - XTypeGroup( - label: 'OGG', - extensions: ['ogg', 'OGG'], + label: 'Videos', + extensions: [ + 'mp4', + 'MP4', + 'avi', + 'AVI', + 'webm', + 'WebM', + 'WEBM', + 'mov', + 'MOV', + 'mkv', + 'MKV', + 'wmv', + 'WMV', + 'flv', + 'FLV', + 'mpeg', + 'MPEG', + '3gp', + '3GP', + 'ogg', + 'OGG', + ], ), + XTypeGroup(label: 'MP4', extensions: ['mp4', 'MP4']), + XTypeGroup(label: 'WebM', extensions: ['webm', 'WebM', 'WEBM']), + XTypeGroup(label: 'AVI', extensions: ['avi', 'AVI']), + XTypeGroup(label: 'MOV', extensions: ['mov', 'MOV']), + XTypeGroup(label: 'MKV', extensions: ['mkv', 'MKV']), + XTypeGroup(label: 'WMV', extensions: ['wmv', 'WMV']), + XTypeGroup(label: 'FLV', extensions: ['flv', 'FLV']), + XTypeGroup(label: 'MPEG', extensions: ['mpeg', 'MPEG']), + XTypeGroup(label: '3GP', extensions: ['3gp', '3GP']), + XTypeGroup(label: 'OGG', extensions: ['ogg', 'OGG']), ], FileType.video, null, ), zip( [ - XTypeGroup( - label: 'ZIP', - extensions: ['zip', 'ZIP'], - ), + XTypeGroup(label: 'ZIP', extensions: ['zip', 'ZIP']), ], FileType.custom, ['zip', 'ZIP'], diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index 4781c573b..fa1953f80 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -33,6 +33,12 @@ abstract class PlatformInfos { static bool get usesTouchscreen => !isMobile; + static bool get supportsVideoPlayer => + // #Pangea + // !PlatformInfos.isWindows && !PlatformInfos.isLinux; + !PlatformInfos.isLinux; + // Pangea# + /// Web could also record in theory but currently only wav which is too large /// #Pangea // static bool get platformCanRecord => (isMobile || isMacOS); diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index d4f15ccec..538ad77d5 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -16,6 +16,7 @@ import 'package:fluffychat/utils/client_download_content_extension.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/shortcut_memory_icon.dart'; Future pushHelper( PushNotification notification, { @@ -319,9 +320,10 @@ Future _setShortcut( action: AppConfig.inviteLinkPrefix + event.room.id, shortLabel: title, conversationShortcut: true, - icon: avatarFile == null - ? null - : ShortcutMemoryIcon(jpegImage: avatarFile).toString(), + icon: await avatarFile?.toShortcutMemoryIcon( + event.room.id, + event.room.client.database, + ), shortcutIconAsset: avatarFile == null ? ShortcutIconAsset.androidAsset : ShortcutIconAsset.memoryAsset, diff --git a/lib/utils/shortcut_memory_icon.dart b/lib/utils/shortcut_memory_icon.dart new file mode 100644 index 000000000..c1557e44a --- /dev/null +++ b/lib/utils/shortcut_memory_icon.dart @@ -0,0 +1,36 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:image/image.dart'; +import 'package:matrix/matrix.dart'; + +extension ShortcutMemoryIcon on Uint8List { + Future toShortcutMemoryIcon( + String roomId, + DatabaseApi? database, + ) async { + final cacheKey = Uri.parse('im.fluffychat://shortcuts/$roomId'); + final cachedFile = await database?.getFile(cacheKey); + if (cachedFile != null) return base64Encode(cachedFile); + + final image = decodeImage(this); + if (image == null) return null; + + final size = image.width < image.height ? image.width : image.height; + final x = (image.width - size) ~/ 2; + final y = (image.height - size) ~/ 2; + + final croppedImage = copyCrop( + image, + x: x, + y: y, + width: size, + height: size, + ); + + final bytes = croppedImage.toUint8List(); + await database?.storeFile(cacheKey, bytes, 0); + + return base64Encode(croppedImage.toUint8List()); + } +} diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index c33a988af..a7e57c14b 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -96,7 +96,7 @@ class _MxcImageState extends State { final data = await event.downloadAndDecryptAttachment( getThumbnail: widget.isThumbnail, ); - if (data.detectFileType is MatrixImageFile) { + if (data.detectFileType is MatrixImageFile || widget.isThumbnail) { if (!mounted) return; setState(() { _imageData = data.bytes; diff --git a/pubspec.yaml b/pubspec.yaml index ab0f1992e..4615a6f99 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: async: ^2.11.0 badges: ^3.1.2 blurhash_dart: ^1.2.1 - chewie: ^1.11.0 + chewie: ^1.11.3 collection: ^1.18.0 cross_file: ^0.3.4+2 cupertino_icons: any @@ -103,7 +103,7 @@ dependencies: universal_html: ^2.2.4 url_launcher: ^6.2.5 video_compress: ^3.1.4 - video_player: ^2.9.2 + video_player: ^2.9.5 wakelock_plus: ^1.2.2 webrtc_interface: ^1.0.13 # #Pangea