video_widget_new.dart 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. import "dart:io";
  2. import "package:flutter/cupertino.dart";
  3. import "package:flutter/material.dart";
  4. import "package:media_kit/media_kit.dart";
  5. import "package:media_kit_video/media_kit_video.dart";
  6. import "package:photos/core/constants.dart";
  7. import "package:photos/generated/l10n.dart";
  8. import "package:photos/models/file/extensions/file_props.dart";
  9. import "package:photos/models/file/file.dart";
  10. import "package:photos/services/files_service.dart";
  11. import "package:photos/theme/colors.dart";
  12. import "package:photos/theme/ente_theme.dart";
  13. import "package:photos/ui/viewer/file/thumbnail_widget.dart";
  14. import "package:photos/utils/dialog_util.dart";
  15. import "package:photos/utils/file_util.dart";
  16. import "package:photos/utils/toast_util.dart";
  17. class VideoWidgetNew extends StatefulWidget {
  18. final EnteFile file;
  19. final String? tagPrefix;
  20. const VideoWidgetNew(
  21. this.file, {
  22. this.tagPrefix,
  23. super.key,
  24. });
  25. @override
  26. State<VideoWidgetNew> createState() => _VideoWidgetNewState();
  27. }
  28. class _VideoWidgetNewState extends State<VideoWidgetNew> {
  29. static const verticalMargin = 100.0;
  30. late final player = Player();
  31. VideoController? controller;
  32. final _progressNotifier = ValueNotifier<double?>(null);
  33. @override
  34. void initState() {
  35. super.initState();
  36. if (widget.file.isRemoteFile) {
  37. _loadNetworkVideo();
  38. _setFileSizeIfNull();
  39. } else if (widget.file.isSharedMediaToAppSandbox) {
  40. final localFile = File(getSharedMediaFilePath(widget.file));
  41. if (localFile.existsSync()) {
  42. _setVideoController(localFile.path);
  43. } else if (widget.file.uploadedFileID != null) {
  44. _loadNetworkVideo();
  45. }
  46. } else {
  47. widget.file.getAsset.then((asset) async {
  48. if (asset == null || !(await asset.exists)) {
  49. if (widget.file.uploadedFileID != null) {
  50. _loadNetworkVideo();
  51. }
  52. } else {
  53. asset.getMediaUrl().then((url) {
  54. _setVideoController(
  55. url ??
  56. 'https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4',
  57. );
  58. });
  59. }
  60. });
  61. }
  62. }
  63. @override
  64. void dispose() {
  65. player.dispose();
  66. // _progressNotifier.dispose();
  67. super.dispose();
  68. }
  69. @override
  70. Widget build(BuildContext context) {
  71. final colorScheme = getEnteColorScheme(context);
  72. return Hero(
  73. tag: widget.tagPrefix! + widget.file.tag,
  74. child: MaterialVideoControlsTheme(
  75. normal: MaterialVideoControlsThemeData(
  76. backdropColor: null,
  77. automaticallyImplySkipNextButton: false,
  78. automaticallyImplySkipPreviousButton: false,
  79. seekOnDoubleTap: false,
  80. displaySeekBar: true,
  81. seekBarMargin: const EdgeInsets.only(bottom: verticalMargin),
  82. bottomButtonBarMargin: const EdgeInsets.only(bottom: 112),
  83. controlsHoverDuration: const Duration(seconds: 3),
  84. seekBarHeight: 2,
  85. seekBarThumbSize: 16,
  86. seekBarBufferColor: Colors.transparent,
  87. seekBarThumbColor: backgroundElevatedLight,
  88. seekBarColor: fillMutedDark,
  89. seekBarPositionColor: colorScheme.primary300,
  90. ///topButtonBarMargin is needed for keeping the buffering loading
  91. ///indicator to be center aligned
  92. topButtonBarMargin: const EdgeInsets.only(top: verticalMargin),
  93. bottomButtonBar: [
  94. const Spacer(),
  95. PausePlayAndDuration(controller?.player),
  96. const Spacer(),
  97. ],
  98. primaryButtonBar: [],
  99. ),
  100. fullscreen: const MaterialVideoControlsThemeData(),
  101. child: GestureDetector(
  102. onVerticalDragUpdate: (d) => {
  103. if (d.delta.dy > dragSensitivity) {Navigator.of(context).pop()},
  104. },
  105. child: Center(
  106. child: controller != null
  107. ? Video(
  108. controller: controller!,
  109. )
  110. : _getLoadingWidget(),
  111. ),
  112. ),
  113. ),
  114. );
  115. }
  116. void _loadNetworkVideo() {
  117. getFileFromServer(
  118. widget.file,
  119. progressCallback: (count, total) {
  120. _progressNotifier.value = count / (widget.file.fileSize ?? total);
  121. if (_progressNotifier.value == 1) {
  122. if (mounted) {
  123. showShortToast(context, S.of(context).decryptingVideo);
  124. }
  125. }
  126. },
  127. ).then((file) {
  128. if (file != null) {
  129. _setVideoController(file.path);
  130. }
  131. }).onError((error, stackTrace) {
  132. showErrorDialog(context, "Error", S.of(context).failedToDownloadVideo);
  133. });
  134. }
  135. void _setFileSizeIfNull() {
  136. if (widget.file.fileSize == null && widget.file.canEditMetaInfo) {
  137. FilesService.instance
  138. .getFileSize(widget.file.uploadedFileID!)
  139. .then((value) {
  140. widget.file.fileSize = value;
  141. if (mounted) {
  142. setState(() {});
  143. }
  144. });
  145. }
  146. }
  147. Widget _getLoadingWidget() {
  148. return Stack(
  149. children: [
  150. _getThumbnail(),
  151. Container(
  152. color: Colors.black12,
  153. constraints: const BoxConstraints.expand(),
  154. ),
  155. Center(
  156. child: SizedBox.fromSize(
  157. size: const Size.square(20),
  158. child: ValueListenableBuilder(
  159. valueListenable: _progressNotifier,
  160. builder: (BuildContext context, double? progress, _) {
  161. return progress == null || progress == 1
  162. ? const CupertinoActivityIndicator(
  163. color: Colors.white,
  164. )
  165. : CircularProgressIndicator(
  166. backgroundColor: Colors.black,
  167. value: progress,
  168. valueColor: const AlwaysStoppedAnimation<Color>(
  169. Color.fromRGBO(45, 194, 98, 1.0),
  170. ),
  171. );
  172. },
  173. ),
  174. ),
  175. ),
  176. ],
  177. );
  178. }
  179. Widget _getThumbnail() {
  180. return Container(
  181. color: Colors.black,
  182. constraints: const BoxConstraints.expand(),
  183. child: ThumbnailWidget(
  184. widget.file,
  185. fit: BoxFit.contain,
  186. ),
  187. );
  188. }
  189. void _setVideoController(String url) {
  190. if (mounted) {
  191. setState(() {
  192. controller = VideoController(player);
  193. player.open(Media(url));
  194. });
  195. }
  196. }
  197. }
  198. class PausePlayAndDuration extends StatefulWidget {
  199. final Player? player;
  200. const PausePlayAndDuration(this.player, {super.key});
  201. @override
  202. State<PausePlayAndDuration> createState() => _PausePlayAndDurationState();
  203. }
  204. class _PausePlayAndDurationState extends State<PausePlayAndDuration> {
  205. Color backgroundColor = fillStrongLight;
  206. @override
  207. Widget build(BuildContext context) {
  208. return GestureDetector(
  209. onTapDown: (details) {
  210. setState(() {
  211. backgroundColor = fillMutedDark;
  212. });
  213. },
  214. onTapUp: (details) {
  215. Future.delayed(const Duration(milliseconds: 175), () {
  216. if (mounted) {
  217. setState(() {
  218. backgroundColor = fillStrongLight;
  219. });
  220. }
  221. });
  222. },
  223. onTapCancel: () {
  224. Future.delayed(const Duration(milliseconds: 175), () {
  225. if (mounted) {
  226. setState(() {
  227. backgroundColor = fillStrongLight;
  228. });
  229. }
  230. });
  231. },
  232. onTap: () => widget.player!.playOrPause(),
  233. child: AnimatedContainer(
  234. duration: const Duration(milliseconds: 150),
  235. curve: Curves.easeInBack,
  236. padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
  237. decoration: BoxDecoration(
  238. color: backgroundColor,
  239. border: Border.all(
  240. color: strokeFaintDark,
  241. width: 1,
  242. ),
  243. borderRadius: BorderRadius.circular(24),
  244. ),
  245. child: AnimatedSize(
  246. duration: const Duration(seconds: 2),
  247. curve: Curves.easeInOutExpo,
  248. child: Row(
  249. children: [
  250. StreamBuilder(
  251. builder: (context, snapshot) {
  252. final bool isPlaying = snapshot.data ?? false;
  253. return AnimatedSwitcher(
  254. duration: const Duration(milliseconds: 350),
  255. switchInCurve: Curves.easeInOutCirc,
  256. switchOutCurve: Curves.easeInOutCirc,
  257. child: Icon(
  258. key: ValueKey(
  259. isPlaying ? "pause_button" : "play_button",
  260. ),
  261. isPlaying
  262. ? Icons.pause_rounded
  263. : Icons.play_arrow_rounded,
  264. color: backdropBaseLight,
  265. size: 24,
  266. ),
  267. );
  268. },
  269. initialData: widget.player?.state.playing,
  270. stream: widget.player?.stream.playing,
  271. ),
  272. const SizedBox(width: 8),
  273. MaterialPositionIndicator(
  274. style: getEnteTextTheme(context).tiny.copyWith(
  275. color: textBaseDark,
  276. ),
  277. ),
  278. const SizedBox(width: 10),
  279. ],
  280. ),
  281. ),
  282. ),
  283. );
  284. }
  285. }