From 1bae9186a7ab01384e36094c4f3886eb37168104 Mon Sep 17 00:00:00 2001 From: MinhDV Date: Fri, 22 Sep 2023 16:59:57 +0700 Subject: [PATCH] TW-616 Video player for media page --- lib/pages/chat/events/event_video_player.dart | 49 +++++++++- lib/pages/chat/events/image_bubble.dart | 25 ++++- lib/pages/chat/events/message_content.dart | 7 +- lib/pages/chat_details/chat_details.dart | 13 ++- .../media/chat_details_media_page.dart | 97 ++++++++++++------- .../mixins/play_video_action_mixin.dart | 1 + lib/widgets/mxc_image.dart | 9 +- lib/widgets/video_viewer_dialog.dart | 12 ++- 8 files changed, 159 insertions(+), 54 deletions(-) diff --git a/lib/pages/chat/events/event_video_player.dart b/lib/pages/chat/events/event_video_player.dart index 4a51dfee16..994ecf7861 100644 --- a/lib/pages/chat/events/event_video_player.dart +++ b/lib/pages/chat/events/event_video_player.dart @@ -1,8 +1,10 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/download_video_state.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; +import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart'; import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; @@ -11,6 +13,9 @@ import 'package:matrix/matrix.dart'; import 'package:fluffychat/pages/chat/events/image_bubble.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:linagora_design_flutter/extensions/duration_extension.dart'; + +typedef DownloadVideoEventCallback = Future Function(Event event); class EventVideoPlayer extends StatefulWidget { final Event event; @@ -19,8 +24,18 @@ class EventVideoPlayer extends StatefulWidget { final double? height; - final Future Function({required Event event})? - handleDownloadVideoEvent; + final bool rounded; + + final bool showDuration; + + final DownloadVideoEventCallback? handleDownloadVideoEvent; + + final String? thumbnailCacheKey; + + final Map? thumbnailCacheMap; + + /// Enable it if the thumbnail image is stretched, and you don't want to resize it + final bool noResizeThumbnail; const EventVideoPlayer( this.event, { @@ -28,6 +43,11 @@ class EventVideoPlayer extends StatefulWidget { this.width, this.height, this.handleDownloadVideoEvent, + this.rounded = true, + this.showDuration = false, + this.thumbnailCacheMap, + this.thumbnailCacheKey, + this.noResizeThumbnail = false, }) : super(key: key); @override @@ -42,7 +62,7 @@ class EventVideoPlayerState extends State void _downloadAction() async { _downloadStateNotifier.value = DownloadVideoState.loading; try { - path = await widget.handleDownloadVideoEvent?.call(event: widget.event); + path = await widget.handleDownloadVideoEvent?.call(widget.event); _downloadStateNotifier.value = DownloadVideoState.done; } on MatrixConnectionException catch (e) { _downloadStateNotifier.value = DownloadVideoState.failed; @@ -71,7 +91,9 @@ class EventVideoPlayerState extends State final height = widget.height ?? MessageContentStyle.imageHeight(context); return ClipRRect( - borderRadius: MessageContentStyle.borderRadiusBubble, + borderRadius: widget.rounded + ? MessageContentStyle.borderRadiusBubble + : BorderRadius.zero, child: Material( color: Colors.black, child: SizedBox( @@ -87,6 +109,10 @@ class EventVideoPlayerState extends State tapToView: false, width: MessageContentStyle.imageBubbleWidth(width), height: MessageContentStyle.videoBubbleHeight(height), + rounded: widget.rounded, + thumbnailCacheKey: widget.thumbnailCacheKey, + thumbnailCacheMap: widget.thumbnailCacheMap, + noResizeThumbnail: widget.noResizeThumbnail, ), ) else @@ -138,6 +164,21 @@ class EventVideoPlayerState extends State }, ), ), + if (widget.showDuration) + Positioned( + bottom: ChatDetailsMediaStyle.durationPosition, + right: ChatDetailsMediaStyle.durationPosition, + child: Container( + padding: ChatDetailsMediaStyle.durationPadding, + decoration: ChatDetailsMediaStyle.durationBoxDecoration( + context, + ), + child: Text( + widget.event.duration?.mediaTimeLength() ?? "--:--", + style: ChatDetailsMediaStyle.durationTextStyle(context), + ), + ), + ), ], ), ), diff --git a/lib/pages/chat/events/image_bubble.dart b/lib/pages/chat/events/image_bubble.dart index 84804ad0e6..1900cba48f 100644 --- a/lib/pages/chat/events/image_bubble.dart +++ b/lib/pages/chat/events/image_bubble.dart @@ -18,11 +18,16 @@ class ImageBubble extends StatelessWidget { final bool animated; final double width; final double height; + final bool rounded; final void Function()? onTapPreview; final void Function()? onTapSelectMode; final Uint8List? imageData; final Duration animationDuration; + final String? thumbnailCacheKey; + final Map? thumbnailCacheMap; + final bool noResizeThumbnail; + const ImageBubble( this.event, { this.imageData, @@ -34,9 +39,13 @@ class ImageBubble extends StatelessWidget { this.width = 256, this.height = 300, this.animated = false, + this.rounded = true, this.onTapSelectMode, this.onTapPreview, this.animationDuration = const Duration(milliseconds: 500), + this.thumbnailCacheKey, + this.thumbnailCacheMap, + this.noResizeThumbnail = false, Key? key, }) : super(key: key); @@ -59,7 +68,8 @@ class ImageBubble extends StatelessWidget { width = (height * ratio).round(); } return ClipRRect( - borderRadius: BorderRadius.circular(12), + borderRadius: + rounded ? MessageContentStyle.borderRadiusBubble : BorderRadius.zero, child: SizedBox( width: this.width, height: this.height, @@ -82,8 +92,10 @@ class ImageBubble extends StatelessWidget { child: AnimatedSwitcher( duration: const Duration(seconds: 1), child: Container( - decoration: const BoxDecoration( - borderRadius: MessageContentStyle.borderRadiusBubble, + decoration: BoxDecoration( + borderRadius: rounded + ? MessageContentStyle.borderRadiusBubble + : BorderRadius.zero, ), constraints: maxSize ? BoxConstraints( @@ -92,7 +104,9 @@ class ImageBubble extends StatelessWidget { ) : null, child: ClipRRect( - borderRadius: MessageContentStyle.borderRadiusBubble, + borderRadius: rounded + ? MessageContentStyle.borderRadiusBubble + : BorderRadius.zero, child: Stack( alignment: Alignment.center, children: [ @@ -115,6 +129,9 @@ class ImageBubble extends StatelessWidget { imageData: imageData, isPreview: true, animationDuration: animationDuration, + cacheKey: thumbnailCacheKey, + cacheMap: thumbnailCacheMap, + noResize: noResizeThumbnail, ), ], ), diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index f9b40e6568..2ffc3006bd 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -163,12 +163,12 @@ class MessageContent extends StatelessWidget with PlayVideoActionMixin { return _MessageVideoBuilder( event: event, onFileTapped: controller.onFileTapped, - handleDownloadVideoEvent: (({required Event event}) { + handleDownloadVideoEvent: (event) { return controller.handleDownloadVideoEvent( event: event, playVideoAction: (path) => playVideoAction(context, path), ); - }), + }, ); case MessageTypes.File: return Column( @@ -500,8 +500,7 @@ class _MessageVideoBuilder extends StatelessWidget { final void Function(Event event) onFileTapped; - final Future Function({required Event event}) - handleDownloadVideoEvent; + final DownloadVideoEventCallback handleDownloadVideoEvent; const _MessageVideoBuilder({ required this.event, diff --git a/lib/pages/chat_details/chat_details.dart b/lib/pages/chat_details/chat_details.dart index 845b1a504e..b133c791c3 100644 --- a/lib/pages/chat_details/chat_details.dart +++ b/lib/pages/chat_details/chat_details.dart @@ -9,6 +9,8 @@ import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_ import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart'; import 'package:fluffychat/pages/invitation_selection/invitation_selection_web.dart'; import 'package:fluffychat/presentation/extensions/room_summary_extension.dart'; +import 'package:fluffychat/presentation/mixins/handle_video_download_mixin.dart'; +import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; import 'package:fluffychat/presentation/model/chat_details/chat_details_page_model.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; @@ -41,7 +43,8 @@ class ChatDetails extends StatefulWidget { ChatDetailsController createState() => ChatDetailsController(); } -class ChatDetailsController extends State { +class ChatDetailsController extends State + with HandleVideoDownloadMixin, PlayVideoActionMixin { final invitationSelectionMobileAndTabletKey = const Key('InvitationSelectionMobileAndTabletKey'); @@ -453,6 +456,7 @@ class ChatDetailsController extends State { child: ChatDetailsMediaPage( getTimeline: getTimeline, cacheMap: _mediaCacheMap, + handleDownloadVideoEvent: _handleDownloadAndPlayVideo, ), ), const ChatDetailsPageModel( @@ -471,6 +475,13 @@ class ChatDetailsController extends State { ), ]; + Future _handleDownloadAndPlayVideo(Event event) { + return handleDownloadVideoEvent( + event: event, + playVideoAction: (path) => playVideoAction(context, path), + ); + } + void onTapActionsButton(ChatDetailsActions action) { switch (action) { case ChatDetailsActions.addMembers: diff --git a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart index e888b3fd4b..e8c37b9c68 100644 --- a/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart +++ b/lib/pages/chat_details/chat_details_page_view/media/chat_details_media_page.dart @@ -1,22 +1,24 @@ import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/domain/app_state/room/timeline_search_event_state.dart'; -import 'package:fluffychat/pages/chat_details/chat_details_page_view/media/chat_details_media_style.dart'; +import 'package:fluffychat/pages/chat/events/event_video_player.dart'; import 'package:fluffychat/pages/chat_details/chat_details_page_view/same_type_events_list_controller.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blurhash/flutter_blurhash.dart'; -import 'package:linagora_design_flutter/extensions/duration_extension.dart'; import 'package:matrix/matrix.dart'; class ChatDetailsMediaPage extends StatelessWidget { static const _mediaFetchLimit = 20; final Future Function() getTimeline; final Map? cacheMap; + final DownloadVideoEventCallback handleDownloadVideoEvent; + const ChatDetailsMediaPage({ Key? key, required this.getTimeline, + required this.handleDownloadVideoEvent, this.cacheMap, }) : super(key: key); @@ -37,41 +39,68 @@ class ChatDetailsMediaPage extends StatelessWidget { crossAxisCount: 3, ), itemCount: events.length, - itemBuilder: (context, index) => Stack( - fit: StackFit.expand, - children: [ - MxcImage( - event: events[index], - isThumbnail: true, - fit: BoxFit.cover, - onTapPreview: () {}, - isPreview: true, - placeholder: (context) => BlurHash( - hash: - events[index].blurHash ?? AppConfig.defaultImageBlurHash, - ), - cacheKey: events[index].eventId, - cacheMap: cacheMap, - ), - if (events[index].messageType == MessageTypes.Video) - Positioned( - bottom: ChatDetailsMediaStyle.durationPosition, - right: ChatDetailsMediaStyle.durationPosition, - child: Container( - padding: ChatDetailsMediaStyle.durationPadding, - decoration: ChatDetailsMediaStyle.durationBoxDecoration( - context, - ), - child: Text( - events[index].duration?.mediaTimeLength() ?? "--:--", - style: ChatDetailsMediaStyle.durationTextStyle(context), + itemBuilder: (context, index) => + events[index].messageType == MessageTypes.Image + ? _ImageItem( + event: events[index], + cacheMap: cacheMap, + ) + : _VideoItem( + event: events[index], + handleDownloadVideoEvent: handleDownloadVideoEvent, + thumbnailCacheMap: cacheMap, ), - ), - ), - ], - ), ); }, ); } } + +class _ImageItem extends StatelessWidget { + final Event event; + final Map? cacheMap; + const _ImageItem({ + required this.event, + this.cacheMap, + }); + + @override + Widget build(BuildContext context) { + return MxcImage( + event: event, + isThumbnail: true, + fit: BoxFit.cover, + onTapPreview: () {}, + isPreview: true, + placeholder: (context) => BlurHash( + hash: event.blurHash ?? AppConfig.defaultImageBlurHash, + ), + cacheKey: event.eventId, + cacheMap: cacheMap, + ); + } +} + +class _VideoItem extends StatelessWidget { + final Event event; + final DownloadVideoEventCallback handleDownloadVideoEvent; + final Map? thumbnailCacheMap; + const _VideoItem({ + required this.event, + required this.handleDownloadVideoEvent, + this.thumbnailCacheMap, + }); + + @override + Widget build(BuildContext context) { + return EventVideoPlayer( + event, + handleDownloadVideoEvent: handleDownloadVideoEvent, + rounded: false, + showDuration: true, + thumbnailCacheKey: event.eventId, + thumbnailCacheMap: thumbnailCacheMap, + noResizeThumbnail: true, + ); + } +} diff --git a/lib/presentation/mixins/play_video_action_mixin.dart b/lib/presentation/mixins/play_video_action_mixin.dart index 65ab8077b0..ece2007982 100644 --- a/lib/presentation/mixins/play_video_action_mixin.dart +++ b/lib/presentation/mixins/play_video_action_mixin.dart @@ -7,6 +7,7 @@ mixin PlayVideoActionMixin { await showDialog( context: context, useRootNavigator: PlatformInfos.isWeb, + useSafeArea: false, builder: (context) { return VideoViewerDialog(path: uriOrFilePath); }, diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index f7df9ed94d..ef669b4dee 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -30,6 +30,9 @@ class MxcImage extends StatefulWidget { final ImageData? imageData; final bool isPreview; + /// Enable it if the image is stretched, and you don't want to resize it + final bool noResize; + /// Cache for screen locally, if null, use global cache final Map? cacheMap; @@ -53,6 +56,7 @@ class MxcImage extends StatefulWidget { this.imageData, this.isPreview = false, this.cacheMap, + this.noResize = false, Key? key, }) : super(key: key); @@ -242,6 +246,7 @@ class _MxcImageState extends State Widget _buildImageWidget() { final data = _imageData; + final needResize = widget.event != null && !widget.noResize; return data == null || data.isEmpty ? placeholder(context) : ClipRRect( @@ -253,8 +258,8 @@ class _MxcImageState extends State data, width: widget.width, height: widget.height, - cacheWidth: widget.event != null ? widget.width?.toInt() : null, - cacheHeight: widget.event != null ? widget.height?.toInt() : null, + cacheWidth: needResize ? widget.width?.toInt() : null, + cacheHeight: needResize ? widget.height?.toInt() : null, fit: widget.fit, filterQuality: FilterQuality.medium, errorBuilder: (context, __, ___) { diff --git a/lib/widgets/video_viewer_dialog.dart b/lib/widgets/video_viewer_dialog.dart index 48a3117601..827b743019 100644 --- a/lib/widgets/video_viewer_dialog.dart +++ b/lib/widgets/video_viewer_dialog.dart @@ -90,11 +90,13 @@ class _VideoViewerDialogState extends State { color: MessageContentStyle.backgroundColorVideo, ), padding: VideoViewerDialogStyle.bottomPaddingVideo, - child: Video( - pauseUponEnteringBackgroundMode: true, - resumeUponEnteringForegroundMode: true, - controls: MaterialVideoControls, - controller: videoController!, + child: SafeArea( + child: Video( + pauseUponEnteringBackgroundMode: true, + resumeUponEnteringForegroundMode: true, + controls: MaterialVideoControls, + controller: videoController!, + ), ), ) : const SizedBox(),