diff --git a/lib/ui/video_controls.dart b/lib/ui/video_controls.dart new file mode 100644 index 000000000..ffc731f01 --- /dev/null +++ b/lib/ui/video_controls.dart @@ -0,0 +1,299 @@ +import 'dart:async'; + +import 'package:chewie/chewie.dart'; +import 'package:chewie/src/material_progress_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:photos/utils/date_time_util.dart'; +import 'package:video_player/video_player.dart'; + +class VideoControls extends StatefulWidget { + const VideoControls({Key key}) : super(key: key); + + @override + State createState() { + return _VideoControlsState(); + } +} + +class _VideoControlsState extends State { + VideoPlayerValue _latestValue; + bool _hideStuff = true; + Timer _hideTimer; + Timer _initTimer; + Timer _showAfterExpandCollapseTimer; + bool _dragging = false; + bool _displayTapped = false; + + final barHeight = 48.0; + final marginSize = 5.0; + + VideoPlayerController controller; + ChewieController chewieController; + + @override + Widget build(BuildContext context) { + if (_latestValue.hasError) { + return chewieController.errorBuilder != null + ? chewieController.errorBuilder( + context, + chewieController.videoPlayerController.value.errorDescription, + ) + : Center( + child: Icon( + Icons.error, + color: Colors.white, + size: 42, + ), + ); + } + + return MouseRegion( + onHover: (_) { + _cancelAndRestartTimer(); + }, + child: GestureDetector( + onTap: () => _cancelAndRestartTimer(), + child: AbsorbPointer( + absorbing: _hideStuff, + child: Column( + children: [ + _latestValue != null && + !_latestValue.isPlaying && + _latestValue.duration == null || + _latestValue.isBuffering + ? const Expanded( + child: const Center( + child: const CircularProgressIndicator(), + ), + ) + : _buildHitArea(), + _buildBottomBar(context), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _dispose(); + super.dispose(); + } + + void _dispose() { + controller.removeListener(_updateState); + _hideTimer?.cancel(); + _initTimer?.cancel(); + _showAfterExpandCollapseTimer?.cancel(); + } + + @override + void didChangeDependencies() { + final _oldController = chewieController; + chewieController = ChewieController.of(context); + controller = chewieController.videoPlayerController; + + if (_oldController != chewieController) { + _dispose(); + _initialize(); + } + + super.didChangeDependencies(); + } + + AnimatedOpacity _buildBottomBar( + BuildContext context, + ) { + final iconColor = Theme.of(context).textTheme.button.color; + + return AnimatedOpacity( + opacity: _hideStuff ? 0.0 : 1.0, + duration: Duration(milliseconds: 300), + child: Container( + height: barHeight, + color: Colors.transparent, + child: Row( + children: [ + _buildCurrentPosition(iconColor), + chewieController.isLive ? const SizedBox() : _buildProgressBar(), + _buildTotalDuration(iconColor), + ], + ), + ), + ); + } + + Expanded _buildHitArea() { + return Expanded( + child: GestureDetector( + onTap: () { + if (_latestValue != null) { + if (_displayTapped) { + setState(() { + _hideStuff = true; + }); + } else + _cancelAndRestartTimer(); + } else { + _playPause(); + + setState(() { + _hideStuff = true; + }); + } + }, + child: Container( + color: Colors.transparent, + child: Center( + child: AnimatedOpacity( + opacity: + _latestValue != null && !_hideStuff && !_dragging ? 1.0 : 0.0, + duration: Duration(milliseconds: 300), + child: GestureDetector( + onTap: _playPause, + child: Padding( + padding: EdgeInsets.all(12.0), + child: Icon( + _latestValue.isPlaying ? Icons.pause : Icons.play_arrow, + size: 64.0), + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildCurrentPosition(Color iconColor) { + final position = _latestValue != null && _latestValue.position != null + ? _latestValue.position + : Duration.zero; + + return Container( + margin: EdgeInsets.only(left: 20.0, right: 16.0), + child: Text( + '${formatDuration(position)}', + style: TextStyle( + fontSize: 12.0, + ), + ), + ); + } + + Widget _buildTotalDuration(Color iconColor) { + final duration = _latestValue != null && _latestValue.duration != null + ? _latestValue.duration + : Duration.zero; + + return Padding( + padding: EdgeInsets.only(right: 20.0), + child: Text( + '${formatDuration(duration)}', + style: TextStyle( + fontSize: 12.0, + ), + ), + ); + } + + void _cancelAndRestartTimer() { + _hideTimer?.cancel(); + _startHideTimer(); + + setState(() { + _hideStuff = false; + _displayTapped = true; + }); + } + + Future _initialize() async { + controller.addListener(_updateState); + + _updateState(); + + if ((controller.value != null && controller.value.isPlaying) || + chewieController.autoPlay) { + _startHideTimer(); + } + + if (chewieController.showControlsOnInitialize) { + _initTimer = Timer(Duration(milliseconds: 200), () { + setState(() { + _hideStuff = false; + }); + }); + } + } + + void _playPause() { + bool isFinished = _latestValue.position >= _latestValue.duration; + + setState(() { + if (controller.value.isPlaying) { + _hideStuff = false; + _hideTimer?.cancel(); + controller.pause(); + } else { + _cancelAndRestartTimer(); + + if (!controller.value.initialized) { + controller.initialize().then((_) { + controller.play(); + }); + } else { + if (isFinished) { + controller.seekTo(Duration(seconds: 0)); + } + controller.play(); + } + } + }); + } + + void _startHideTimer() { + _hideTimer = Timer(const Duration(seconds: 2), () { + setState(() { + _hideStuff = true; + }); + }); + } + + void _updateState() { + setState(() { + _latestValue = controller.value; + }); + } + + Widget _buildProgressBar() { + return Expanded( + child: Padding( + padding: EdgeInsets.only(right: 16.0), + child: MaterialVideoProgressBar( + controller, + onDragStart: () { + setState(() { + _dragging = true; + }); + + _hideTimer?.cancel(); + }, + onDragEnd: () { + setState(() { + _dragging = false; + }); + + _startHideTimer(); + }, + colors: chewieController.materialProgressColors ?? + ChewieProgressColors( + playedColor: Colors.white, + handleColor: Colors.white, + bufferedColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).disabledColor), + ), + ), + ); + } +} diff --git a/lib/ui/video_widget.dart b/lib/ui/video_widget.dart index 66906e47e..4c2df9d53 100644 --- a/lib/ui/video_widget.dart +++ b/lib/ui/video_widget.dart @@ -1,6 +1,8 @@ import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:photos/models/file.dart'; +import 'package:photos/ui/video_controls.dart'; import 'package:video_player/video_player.dart'; import 'loading_widget.dart'; @@ -61,6 +63,7 @@ class _VideoWidgetState extends State { autoInitialize: true, looping: true, allowFullScreen: false, + customControls: VideoControls(), ); } } diff --git a/lib/utils/date_time_util.dart b/lib/utils/date_time_util.dart index 75d0bb726..a18ff3e26 100644 --- a/lib/utils/date_time_util.dart +++ b/lib/utils/date_time_util.dart @@ -64,3 +64,26 @@ String getFormattedTime(DateTime dateTime) { " - " + getTime(dateTime); } + +String formatDuration(Duration position) { + final ms = position.inMilliseconds; + + int seconds = ms ~/ 1000; + final int hours = seconds ~/ 3600; + seconds = seconds % 3600; + var minutes = seconds ~/ 60; + seconds = seconds % 60; + + final hoursString = hours >= 10 ? '$hours' : hours == 0 ? '00' : '0$hours'; + + final minutesString = + minutes >= 10 ? '$minutes' : minutes == 0 ? '00' : '0$minutes'; + + final secondsString = + seconds >= 10 ? '$seconds' : seconds == 0 ? '00' : '0$seconds'; + + final formattedTime = + '${hoursString == '00' ? '' : hoursString + ':'}$minutesString:$secondsString'; + + return formattedTime; +}