video_controls.dart 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import 'dart:async';
  2. import 'package:chewie/chewie.dart';
  3. import 'package:chewie/src/material/material_progress_bar.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:photos/ente_theme_data.dart';
  6. import 'package:photos/utils/date_time_util.dart';
  7. import 'package:video_player/video_player.dart';
  8. class VideoControls extends StatefulWidget {
  9. const VideoControls({Key? key}) : super(key: key);
  10. @override
  11. State<StatefulWidget> createState() {
  12. return _VideoControlsState();
  13. }
  14. }
  15. class _VideoControlsState extends State<VideoControls> {
  16. VideoPlayerValue? _latestValue;
  17. bool _hideStuff = true;
  18. Timer? _hideTimer;
  19. Timer? _initTimer;
  20. Timer? _showAfterExpandCollapseTimer;
  21. bool _dragging = false;
  22. bool _displayTapped = false;
  23. final barHeight = 120.0;
  24. final marginSize = 5.0;
  25. late VideoPlayerController controller;
  26. ChewieController? chewieController;
  27. @override
  28. Widget build(BuildContext context) {
  29. if (_latestValue!.hasError) {
  30. return chewieController!.errorBuilder != null
  31. ? chewieController!.errorBuilder!(
  32. context,
  33. chewieController!.videoPlayerController.value.errorDescription!,
  34. )
  35. : Center(
  36. child: Icon(
  37. Icons.error,
  38. color: Theme.of(context).colorScheme.onSurface,
  39. size: 42,
  40. ),
  41. );
  42. }
  43. return MouseRegion(
  44. onHover: (_) {
  45. _cancelAndRestartTimer();
  46. },
  47. child: GestureDetector(
  48. onTap: () => _cancelAndRestartTimer(),
  49. child: AbsorbPointer(
  50. absorbing: _hideStuff,
  51. child: Stack(
  52. children: <Widget>[
  53. Column(
  54. children: [
  55. _latestValue != null &&
  56. !_latestValue!.isPlaying &&
  57. _latestValue!.isBuffering
  58. ? const Center(
  59. child: CircularProgressIndicator(),
  60. )
  61. : _buildHitArea(),
  62. ],
  63. ),
  64. Align(
  65. alignment: Alignment.bottomCenter,
  66. child: _buildBottomBar(context),
  67. ),
  68. ],
  69. ),
  70. ),
  71. ),
  72. );
  73. }
  74. @override
  75. void dispose() {
  76. _dispose();
  77. super.dispose();
  78. }
  79. void _dispose() {
  80. controller.removeListener(_updateState);
  81. _hideTimer?.cancel();
  82. _initTimer?.cancel();
  83. _showAfterExpandCollapseTimer?.cancel();
  84. }
  85. @override
  86. void didChangeDependencies() {
  87. final oldController = chewieController;
  88. chewieController = ChewieController.of(context);
  89. controller = chewieController!.videoPlayerController;
  90. if (oldController != chewieController) {
  91. _dispose();
  92. _initialize();
  93. }
  94. super.didChangeDependencies();
  95. }
  96. Widget _buildBottomBar(
  97. BuildContext context,
  98. ) {
  99. final iconColor = Theme.of(context).textTheme.button!.color;
  100. return Container(
  101. padding: const EdgeInsets.only(bottom: 60),
  102. child: AnimatedOpacity(
  103. opacity: _hideStuff ? 0.0 : 1.0,
  104. duration: const Duration(milliseconds: 300),
  105. child: Container(
  106. height: barHeight,
  107. color: Colors.transparent,
  108. child: Row(
  109. children: <Widget>[
  110. _buildCurrentPosition(iconColor),
  111. chewieController!.isLive ? const SizedBox() : _buildProgressBar(),
  112. _buildTotalDuration(iconColor),
  113. ],
  114. ),
  115. ),
  116. ),
  117. );
  118. }
  119. Expanded _buildHitArea() {
  120. return Expanded(
  121. child: GestureDetector(
  122. onTap: () {
  123. if (_latestValue != null) {
  124. if (_displayTapped) {
  125. setState(() {
  126. _hideStuff = true;
  127. });
  128. } else {
  129. _cancelAndRestartTimer();
  130. }
  131. } else {
  132. _playPause();
  133. setState(() {
  134. _hideStuff = true;
  135. });
  136. }
  137. },
  138. child: Container(
  139. color: Colors.transparent,
  140. child: Center(
  141. child: AnimatedOpacity(
  142. opacity:
  143. _latestValue != null && !_hideStuff && !_dragging ? 1.0 : 0.0,
  144. duration: const Duration(milliseconds: 300),
  145. child: GestureDetector(
  146. onTap: _playPause,
  147. child: Padding(
  148. padding: const EdgeInsets.all(12.0),
  149. child: Icon(
  150. _latestValue!.isPlaying ? Icons.pause : Icons.play_arrow,
  151. color: Colors.white, // same for both themes
  152. size: 64.0,
  153. ),
  154. ),
  155. ),
  156. ),
  157. ),
  158. ),
  159. ),
  160. );
  161. }
  162. Widget _buildCurrentPosition(Color? iconColor) {
  163. final position =
  164. _latestValue != null ? _latestValue!.position : Duration.zero;
  165. return Container(
  166. margin: const EdgeInsets.only(left: 20.0, right: 16.0),
  167. child: Text(
  168. formatDuration(position),
  169. style: const TextStyle(
  170. fontSize: 12.0,
  171. color: Colors.white,
  172. ),
  173. ),
  174. );
  175. }
  176. Widget _buildTotalDuration(Color? iconColor) {
  177. final duration =
  178. _latestValue != null ? _latestValue!.duration : Duration.zero;
  179. return Padding(
  180. padding: const EdgeInsets.only(right: 20.0),
  181. child: Text(
  182. formatDuration(duration),
  183. style: const TextStyle(
  184. fontSize: 12.0,
  185. color: Colors.white,
  186. ),
  187. ),
  188. );
  189. }
  190. void _cancelAndRestartTimer() {
  191. _hideTimer?.cancel();
  192. _startHideTimer();
  193. setState(() {
  194. _hideStuff = false;
  195. _displayTapped = true;
  196. });
  197. }
  198. Future<void> _initialize() async {
  199. controller.addListener(_updateState);
  200. _updateState();
  201. if ((controller.value.isPlaying) || chewieController!.autoPlay) {
  202. _startHideTimer();
  203. }
  204. if (chewieController!.showControlsOnInitialize) {
  205. _initTimer = Timer(const Duration(milliseconds: 200), () {
  206. setState(() {
  207. _hideStuff = false;
  208. });
  209. });
  210. }
  211. }
  212. void _playPause() {
  213. final bool isFinished = _latestValue!.position >= _latestValue!.duration;
  214. setState(() {
  215. if (controller.value.isPlaying) {
  216. _hideStuff = false;
  217. _hideTimer?.cancel();
  218. controller.pause();
  219. } else {
  220. _cancelAndRestartTimer();
  221. if (!controller.value.isInitialized) {
  222. controller.initialize().then((_) {
  223. controller.play();
  224. });
  225. } else {
  226. if (isFinished) {
  227. controller.seekTo(const Duration(seconds: 0));
  228. }
  229. controller.play();
  230. }
  231. }
  232. });
  233. }
  234. void _startHideTimer() {
  235. _hideTimer = Timer(const Duration(seconds: 2), () {
  236. setState(() {
  237. _hideStuff = true;
  238. });
  239. });
  240. }
  241. void _updateState() {
  242. setState(() {
  243. _latestValue = controller.value;
  244. });
  245. }
  246. Widget _buildProgressBar() {
  247. return Expanded(
  248. child: Padding(
  249. padding: const EdgeInsets.only(right: 16.0),
  250. child: MaterialVideoProgressBar(
  251. controller,
  252. onDragStart: () {
  253. setState(() {
  254. _dragging = true;
  255. });
  256. _hideTimer?.cancel();
  257. },
  258. onDragEnd: () {
  259. setState(() {
  260. _dragging = false;
  261. });
  262. _startHideTimer();
  263. },
  264. colors: chewieController!.materialProgressColors ??
  265. ChewieProgressColors(
  266. playedColor: Theme.of(context).colorScheme.greenAlternative,
  267. handleColor: Colors.white,
  268. bufferedColor: Colors.white,
  269. backgroundColor: Theme.of(context).disabledColor,
  270. ),
  271. ),
  272. ),
  273. );
  274. }
  275. }