diff --git a/lib/pages/video_viewer.dart b/lib/pages/video_viewer.dart new file mode 100644 index 00000000..a5d7deed --- /dev/null +++ b/lib/pages/video_viewer.dart @@ -0,0 +1,93 @@ +import 'dart:io'; + +import 'package:matrix/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:vrouter/vrouter.dart'; +import 'package:chewie/chewie.dart'; +import 'package:video_player/video_player.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'views/video_viewer_view.dart'; +import '../widgets/matrix.dart'; +import '../utils/matrix_sdk_extensions.dart/event_extension.dart'; +import '../utils/platform_infos.dart'; + +class VideoViewer extends StatefulWidget { + final Event event; + + const VideoViewer(this.event, {Key key}) : super(key: key); + + @override + VideoViewerController createState() => VideoViewerController(); +} + +class VideoViewerController extends State { + VideoPlayerController videoPlayerController; + ChewieController chewieController; + dynamic error; + + @override + void initState() { + super.initState(); + (() async { + try { + if (widget.event.content['file'] is Map) { + if (PlatformInfos.isWeb) { + throw 'Encrypted videos unavailable in web'; + } + final tempDirectory = (await getTemporaryDirectory()).path; + final mxcUri = widget.event.content + .tryGet>('file') + ?.tryGet('url'); + if (mxcUri == null) { + throw 'No mxc uri found'; + } + // somehow the video viewer doesn't like the uri-encoded slashes, so we'll just gonna replace them with hyphons + final file = File( + '$tempDirectory/videos/${mxcUri.replaceAll(':', '').replaceAll('/', '-')}'); + if (await file.exists() == false) { + final matrixFile = + await widget.event.downloadAndDecryptAttachmentCached(); + await file.create(recursive: true); + await file.writeAsBytes(matrixFile.bytes); + } + videoPlayerController = VideoPlayerController.file(file); + } else if (widget.event.content['url'] is String) { + videoPlayerController = VideoPlayerController.network( + widget.event.getAttachmentUrl()?.toString()); + } else { + throw 'invalid event'; + } + await videoPlayerController.initialize(); + + chewieController = ChewieController( + videoPlayerController: videoPlayerController, + autoPlay: true, + looping: false, + ); + setState(() => null); + } catch (e) { + setState(() => error = e); + } + })(); + } + + @override + void dispose() { + chewieController?.dispose(); + videoPlayerController?.dispose(); + super.dispose(); + } + + /// Forward this video to another room. + void forwardAction() { + Matrix.of(context).shareContent = widget.event.content; + VRouter.of(context).to('/rooms'); + } + + /// Save this file with a system call. + void saveFileAction() => widget.event.saveFile(context); + + @override + Widget build(BuildContext context) => VideoViewerView(this); +} diff --git a/lib/pages/views/video_viewer_view.dart b/lib/pages/views/video_viewer_view.dart new file mode 100644 index 00000000..66fce981 --- /dev/null +++ b/lib/pages/views/video_viewer_view.dart @@ -0,0 +1,51 @@ +import '../video_viewer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:chewie/chewie.dart'; + +class VideoViewerView extends StatelessWidget { + final VideoViewerController controller; + + const VideoViewerView(this.controller, {Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + extendBodyBehindAppBar: true, + appBar: AppBar( + elevation: 0, + leading: IconButton( + icon: Icon(Icons.close), + onPressed: Navigator.of(context).pop, + color: Colors.white, + tooltip: L10n.of(context).close, + ), + backgroundColor: Color(0x44000000), + actions: [ + IconButton( + icon: Icon(Icons.reply_outlined), + onPressed: controller.forwardAction, + color: Colors.white, + tooltip: L10n.of(context).share, + ), + IconButton( + icon: Icon(Icons.download_outlined), + onPressed: controller.saveFileAction, + color: Colors.white, + tooltip: L10n.of(context).downloadFile, + ), + ], + ), + body: Center( + child: controller.error != null + ? Text(controller.error.toString()) + : (controller.chewieController == null + ? CircularProgressIndicator(strokeWidth: 2) + : Chewie( + controller: controller.chewieController, + )), + ), + ); + } +} diff --git a/lib/utils/matrix_sdk_extensions.dart/event_extension.dart b/lib/utils/matrix_sdk_extensions.dart/event_extension.dart index 171c974d..79756a45 100644 --- a/lib/utils/matrix_sdk_extensions.dart/event_extension.dart +++ b/lib/utils/matrix_sdk_extensions.dart/event_extension.dart @@ -38,7 +38,8 @@ extension LocalizedBody on Event { thumbnailInfoMap['size'] < room.client.database.maxFileSize; bool get showThumbnail => - [MessageTypes.Image, MessageTypes.Sticker].contains(messageType) && + [MessageTypes.Image, MessageTypes.Sticker, MessageTypes.Video] + .contains(messageType) && (kIsWeb || isAttachmentSmallEnough || isThumbnailSmallEnough || diff --git a/lib/widgets/event_content/image_bubble.dart b/lib/widgets/event_content/image_bubble.dart index a12c821f..44eef9e7 100644 --- a/lib/widgets/event_content/image_bubble.dart +++ b/lib/widgets/event_content/image_bubble.dart @@ -250,8 +250,19 @@ class _ImageBubbleState extends State { _displayFile.bytes, key: ValueKey(key), fit: widget.fit, - errorBuilder: (context, error, stacktrace) => - getErrorWidget(context, error), + errorBuilder: (context, error, stacktrace) { + if (widget.event.hasThumbnail && !_requestedThumbnailOnFailure) { + _requestedThumbnailOnFailure = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _file = null; + _requestFile(getThumbnail: true); + }); + }); + return getPlaceholderWidget(); + } + return getErrorWidget(context, error); + }, ); } } diff --git a/lib/widgets/event_content/message_content.dart b/lib/widgets/event_content/message_content.dart index a3d1d9f6..f6b2feef 100644 --- a/lib/widgets/event_content/message_content.dart +++ b/lib/widgets/event_content/message_content.dart @@ -14,7 +14,9 @@ import 'package:matrix_link_text/link_text.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../utils/url_launcher.dart'; +import '../../utils/platform_infos.dart'; import '../../config/app_config.dart'; +import '../../pages/video_viewer.dart'; import 'html_message.dart'; import '../matrix.dart'; import 'message_download_content.dart'; @@ -121,6 +123,31 @@ class MessageContent extends StatelessWidget { color: textColor, ); case MessageTypes.Video: + if (event.showThumbnail && + (PlatformInfos.isMobile || PlatformInfos.isWeb)) { + return InkWell( + onTap: () => showDialog( + context: Matrix.of(context).navigatorContext, + useRootNavigator: false, + builder: (_) => VideoViewer(event), + ), + child: Stack( + alignment: Alignment.center, + children: [ + ImageBubble( + event, + width: 400, + height: 300, + fit: BoxFit.cover, + tapToView: false, + ), + Icon(Icons.play_circle_outline, + size: 200, color: Colors.grey), + ], + ), + ); + } + return MessageDownloadContent(event, textColor); case MessageTypes.File: return MessageDownloadContent(event, textColor); case MessageTypes.Text: diff --git a/pubspec.lock b/pubspec.lock index 87e4eee1..4ec43f06 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -120,6 +120,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + chewie: + dependency: "direct main" + description: + name: chewie + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" cli_util: dependency: transitive description: @@ -182,7 +189,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.3" dapackages: dependency: "direct dev" description: @@ -1460,6 +1467,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + video_player: + dependency: transitive + description: + name: video_player + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.12" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" vm_service: dependency: transitive description: @@ -1474,6 +1502,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0+11" + wakelock: + dependency: transitive + description: + name: wakelock + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.3+3" + wakelock_macos: + dependency: transitive + description: + name: wakelock_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0+2" + wakelock_platform_interface: + dependency: transitive + description: + name: wakelock_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1+2" + wakelock_web: + dependency: transitive + description: + name: wakelock_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+2" + wakelock_windows: + dependency: transitive + description: + name: wakelock_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0+1" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 823a5b2d..95c2dfdf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: adaptive_theme: ^2.2.0 audioplayers: ^0.19.1 cached_network_image: ^3.1.0 + chewie: ^1.2.2 cupertino_icons: any desktop_notifications: ">=0.4.0 <0.5.0" # Version 0.5.0 breaks web builds: https://github.com/canonical/dbus.dart/issues/250 email_validator: ^2.0.1