From f409845818f3feaf65edf39538898fda122eaae0 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 26 Nov 2020 18:32:46 +0530 Subject: [PATCH] feat: Now compatible with videos --- lib/src/full_screen_image.dart | 169 +++++++++++++++++++++++-------- lib/src/giphy_attachment.dart | 16 ++- lib/src/image_actions_modal.dart | 25 ++++- lib/src/image_attachment.dart | 8 +- lib/src/image_footer.dart | 19 +++- lib/src/image_group.dart | 6 +- lib/src/image_header.dart | 2 +- lib/src/message_widget.dart | 1 + lib/src/video_attachment.dart | 10 +- 9 files changed, 186 insertions(+), 70 deletions(-) diff --git a/lib/src/full_screen_image.dart b/lib/src/full_screen_image.dart index 8ab0415f..4ddc8513 100644 --- a/lib/src/full_screen_image.dart +++ b/lib/src/full_screen_image.dart @@ -1,16 +1,18 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:photo_view/photo_view.dart'; import 'package:stream_chat_flutter/src/image_footer.dart'; import 'package:stream_chat_flutter/src/image_header.dart'; +import 'package:video_player/video_player.dart'; import '../stream_chat_flutter.dart'; /// A full screen image widget -class FullScreenImage extends StatefulWidget { +class FullScreenMedia extends StatefulWidget { /// The url of the image - final List urls; + final List mediaAttachments; final Message message; final int startIndex; @@ -18,9 +20,9 @@ class FullScreenImage extends StatefulWidget { final DateTime sentAt; /// Instantiate a new FullScreenImage - const FullScreenImage({ + const FullScreenMedia({ Key key, - @required this.urls, + @required this.mediaAttachments, this.message, this.startIndex = 0, this.userName = '', @@ -28,10 +30,10 @@ class FullScreenImage extends StatefulWidget { }) : super(key: key); @override - _FullScreenImageState createState() => _FullScreenImageState(); + _FullScreenMediaState createState() => _FullScreenMediaState(); } -class _FullScreenImageState extends State +class _FullScreenMediaState extends State with SingleTickerProviderStateMixin { bool _optionsShown = true; @@ -40,6 +42,8 @@ class _FullScreenImageState extends State int _currentPage; + List videoPackages = []; + @override void initState() { super.initState(); @@ -47,6 +51,14 @@ class _FullScreenImageState extends State AnimationController(vsync: this, duration: Duration(milliseconds: 300)); _pageController = PageController(initialPage: widget.startIndex); _currentPage = widget.startIndex; + widget.mediaAttachments + .where((element) => element.type == 'video') + .toList() + .forEach((element) { + videoPackages.add(VideoPackage(context, element, () { + setState(() {}); + })); + }); } @override @@ -66,36 +78,63 @@ class _FullScreenImageState extends State }); }, itemBuilder: (context, position) { - return PhotoView( - imageProvider: - CachedNetworkImageProvider(widget.urls[position]), - maxScale: PhotoViewComputedScale.covered, - minScale: PhotoViewComputedScale.contained, - heroAttributes: PhotoViewHeroAttributes( - tag: widget.urls, - ), - backgroundDecoration: BoxDecoration( - color: ColorTween( - begin: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .color, - end: Colors.black) - .lerp(_controller.value), - ), - onTapUp: (a, b, c) { - setState(() { - _optionsShown = !_optionsShown; - }); - if (_controller.isCompleted) { - _controller.reverse(); - } else { - _controller.forward(); - } - }, - ); + if (widget.mediaAttachments[position].type == 'image') { + return PhotoView( + imageProvider: CachedNetworkImageProvider( + widget.mediaAttachments[position].imageUrl ?? + widget.mediaAttachments[position].assetUrl ?? + widget.mediaAttachments[position].thumbUrl), + maxScale: PhotoViewComputedScale.covered, + minScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes( + tag: widget.mediaAttachments, + ), + backgroundDecoration: BoxDecoration( + color: ColorTween( + begin: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .color, + end: Colors.black) + .lerp(_controller.value), + ), + onTapUp: (a, b, c) { + setState(() { + _optionsShown = !_optionsShown; + }); + if (_controller.isCompleted) { + _controller.reverse(); + } else { + _controller.forward(); + } + }, + ); + } else if (widget.mediaAttachments[position].type == + 'video') { + if (!videoPackages[position].initialised) { + return Center( + child: CircularProgressIndicator(), + ); + } + return InkWell( + onTap: () { + setState(() { + _optionsShown = !_optionsShown; + }); + if (_controller.isCompleted) { + _controller.reverse(); + } else { + _controller.forward(); + } + }, + child: Chewie( + controller: videoPackages[position].chewieController, + ), + ); + } + return Container(); }, - itemCount: widget.urls.length, + itemCount: widget.mediaAttachments.length, ); }), AnimatedOpacity( @@ -106,19 +145,20 @@ class _FullScreenImageState extends State children: [ ImageHeader( userName: widget.userName, - sentAt: - 'Sent ${getDay(widget.message.createdAt)} at ${Jiffy(widget.sentAt.toLocal()).format('HH:mm')}', + sentAt: widget.message.createdAt == null + ? '' + : 'Sent ${getDay(widget.message.createdAt)} at ${Jiffy(widget.sentAt.toLocal()).format('HH:mm')}', onBackPressed: () { Navigator.of(context).pop(); }, message: widget.message, - urls: widget.urls, + urls: widget.mediaAttachments, currentIndex: _currentPage, ), ImageFooter( currentPage: _currentPage, - totalPages: widget.urls.length, - urls: widget.urls, + totalPages: widget.mediaAttachments.length, + urls: widget.mediaAttachments, message: widget.message, ), ], @@ -144,4 +184,53 @@ class _FullScreenImageState extends State return 'on ${Jiffy(dateTime).format("MMM do")}'; } } + + @override + void dispose() { + videoPackages.forEach((element) { + element.dispose(); + }); + super.dispose(); + } +} + +class VideoPackage { + VideoPlayerController _videoPlayerController; + ChewieController _chewieController; + bool initialised = false; + VoidCallback onInit; + BuildContext context; + + /// + VideoPackage(this.context, Attachment attachment, this.onInit) { + _videoPlayerController = VideoPlayerController.network(attachment.assetUrl); + _videoPlayerController.initialize().whenComplete(() { + initialised = true; + _chewieController = ChewieController( + videoPlayerController: _videoPlayerController, + autoInitialize: false, + aspectRatio: _videoPlayerController.value.aspectRatio, + ); + onInit(); + }); + + VoidCallback errorListener; + errorListener = () { + if (_videoPlayerController.value.hasError) { + Navigator.pop(context); + launchURL(context, attachment.titleLink); + } + _videoPlayerController.removeListener(errorListener); + }; + _videoPlayerController.addListener(errorListener); + } + + get videoPlayer => _videoPlayerController; + + get chewieController => _chewieController; + + void dispose() { + _videoPlayerController.dispose(); + _chewieController.dispose(); + } } diff --git a/lib/src/giphy_attachment.dart b/lib/src/giphy_attachment.dart index 43e0ce68..323a3ae1 100644 --- a/lib/src/giphy_attachment.dart +++ b/lib/src/giphy_attachment.dart @@ -67,11 +67,9 @@ class GiphyAttachment extends StatelessWidget { return StreamChannel( channel: channel, - child: FullScreenImage( - urls: [ - attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl + child: FullScreenMedia( + mediaAttachments: [ + attachment, ], userName: message.user.name, sentAt: message.createdAt, @@ -295,11 +293,9 @@ class GiphyAttachment extends StatelessWidget { return StreamChannel( channel: channel, - child: FullScreenImage( - urls: [ - attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl + child: FullScreenMedia( + mediaAttachments: [ + attachment, ], userName: message.user.name, sentAt: message.createdAt, diff --git a/lib/src/image_actions_modal.dart b/lib/src/image_actions_modal.dart index c1163af8..415fc22d 100644 --- a/lib/src/image_actions_modal.dart +++ b/lib/src/image_actions_modal.dart @@ -1,16 +1,22 @@ +import 'dart:io'; import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:gallery_saver/gallery_saver.dart'; import '../stream_chat_flutter.dart'; import 'stream_icons.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart'; + class ImageActionsModal extends StatelessWidget { final Message message; final String userName; final String sentAt; - final List urls; + final List urls; final currentIndex; ImageActionsModal( @@ -118,8 +124,11 @@ class ImageActionsModal extends StatelessWidget { }), _buildButton(context, 'Save Image', StreamIcons.save_1, () async { - await GallerySaver.saveImage( - urls[currentIndex].split('?')[0]); + var url = urls[currentIndex].imageUrl ?? + urls[currentIndex].assetUrl ?? + urls[currentIndex].thumbUrl; + var file = await _downloadFile(url); + Navigator.pop(context); }), if (StreamChat.of(context).user.id == message.user.id) @@ -171,4 +180,14 @@ class ImageActionsModal extends StatelessWidget { ), ); } + + static Future _downloadFile(String url) async { + var _client = http.Client(); + var req = await _client.get(Uri.parse(url)); + var bytes = req.bodyBytes; + var dir = (await getTemporaryDirectory()).path; + var file = File('$dir/${basename(url)}'); + await file.writeAsBytes(bytes); + return file; + } } diff --git a/lib/src/image_attachment.dart b/lib/src/image_attachment.dart index 83f747db..a7dde6ef 100644 --- a/lib/src/image_attachment.dart +++ b/lib/src/image_attachment.dart @@ -47,11 +47,9 @@ class ImageAttachment extends StatelessWidget { return StreamChannel( channel: channel, - child: FullScreenImage( - urls: [ - attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl + child: FullScreenMedia( + mediaAttachments: [ + attachment, ], userName: message.user.name, sentAt: message.createdAt, diff --git a/lib/src/image_footer.dart b/lib/src/image_footer.dart index d71ee3af..459e7b26 100644 --- a/lib/src/image_footer.dart +++ b/lib/src/image_footer.dart @@ -36,7 +36,7 @@ class ImageFooter extends StatefulWidget { final int currentPage; final int totalPages; - final List urls; + final List urls; final Message message; /// Creates a channel header @@ -186,7 +186,9 @@ class _ImageFooterState extends State { padding: const EdgeInsets.all(1.0), child: AspectRatio( child: CachedNetworkImage( - imageUrl: widget.urls[position], + imageUrl: widget.urls[position].imageUrl ?? + widget.urls[position].assetUrl ?? + widget.urls[position].thumbUrl, fit: BoxFit.cover, ), aspectRatio: 1.0, @@ -378,8 +380,13 @@ class _ImageFooterState extends State { child: Material( child: InkWell( onTap: () async { - await GallerySaver.saveImage(widget - .urls[widget.currentPage] + await GallerySaver.saveImage((widget + .urls[widget.currentPage] + .imageUrl ?? + widget.urls[widget.currentPage] + .assetUrl ?? + widget.urls[widget.currentPage] + .thumbUrl) .split('?')[0]); Navigator.pop(context); }, @@ -518,7 +525,9 @@ class _ImageFooterState extends State { ), iconSize: 24.0, onPressed: () async { - var url = widget.urls[widget.currentPage]; + var url = widget.urls[widget.currentPage].imageUrl ?? + widget.urls[widget.currentPage].assetUrl ?? + widget.urls[widget.currentPage].thumbUrl; var type = url?.split('?')?.first?.split('.')?.last ?? 'jpg'; var request = await HttpClient().getUrl(Uri.parse(url)); var response = await request.close(); diff --git a/lib/src/image_group.dart b/lib/src/image_group.dart index 1b6e48f2..6702e839 100644 --- a/lib/src/image_group.dart +++ b/lib/src/image_group.dart @@ -115,10 +115,8 @@ class ImageGroup extends StatelessWidget { MaterialPageRoute( builder: (context) => StreamChannel( channel: channel, - child: FullScreenImage( - urls: images - .map((e) => e.imageUrl ?? e.thumbUrl ?? e.assetUrl) - .toList(), + child: FullScreenMedia( + mediaAttachments: images, startIndex: index, userName: message.user.name, sentAt: message.createdAt, diff --git a/lib/src/image_header.dart b/lib/src/image_header.dart index 707750c5..77f56460 100644 --- a/lib/src/image_header.dart +++ b/lib/src/image_header.dart @@ -30,7 +30,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget { final String userName; final String sentAt; - final List urls; + final List urls; final currentIndex; /// Creates a channel header diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 86479ebe..53bc454e 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -180,6 +180,7 @@ class MessageWidget extends StatefulWidget { MediaQuery.of(context).size.width * 0.8, MediaQuery.of(context).size.height * 0.3, ), + message: message, ); }, 'giphy': (context, message, attachment) { diff --git a/lib/src/video_attachment.dart b/lib/src/video_attachment.dart index b0d1f60d..b6caed53 100644 --- a/lib/src/video_attachment.dart +++ b/lib/src/video_attachment.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/full_screen_image.dart'; import 'package:stream_chat_flutter/src/full_screen_video.dart'; import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -13,11 +14,13 @@ class VideoAttachment extends StatefulWidget { final Attachment attachment; final MessageTheme messageTheme; final Size size; + final Message message; VideoAttachment({ Key key, @required this.attachment, @required this.messageTheme, + this.message, this.size, }) : super(key: key); @@ -84,8 +87,11 @@ class _VideoAttachmentState extends State { Navigator.push( context, MaterialPageRoute( - builder: (_) => FullScreenVideo( - attachment: widget.attachment, + builder: (_) => FullScreenMedia( + mediaAttachments: [widget.attachment], + userName: widget.message.user.name, + sentAt: widget.message.createdAt, + message: widget.message, ), ), );