zoomable_live_image.dart 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import "dart:io";
  2. import 'package:chewie/chewie.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:motion_photos/motion_photos.dart';
  6. import "package:photos/generated/l10n.dart";
  7. import "package:photos/models/file/extensions/file_props.dart";
  8. import 'package:photos/models/file/file.dart';
  9. import "package:photos/models/metadata/file_magic.dart";
  10. import "package:photos/services/file_magic_service.dart";
  11. import 'package:photos/ui/viewer/file/zoomable_image.dart';
  12. import 'package:photos/utils/file_util.dart';
  13. import 'package:photos/utils/toast_util.dart';
  14. import 'package:video_player/video_player.dart';
  15. class ZoomableLiveImage extends StatefulWidget {
  16. final EnteFile enteFile;
  17. final Function(bool)? shouldDisableScroll;
  18. final String? tagPrefix;
  19. final Decoration? backgroundDecoration;
  20. const ZoomableLiveImage(
  21. this.enteFile, {
  22. Key? key,
  23. this.shouldDisableScroll,
  24. required this.tagPrefix,
  25. this.backgroundDecoration,
  26. }) : super(key: key);
  27. @override
  28. State<ZoomableLiveImage> createState() => _ZoomableLiveImageState();
  29. }
  30. class _ZoomableLiveImageState extends State<ZoomableLiveImage>
  31. with SingleTickerProviderStateMixin {
  32. final Logger _logger = Logger("ZoomableLiveImage");
  33. late EnteFile _enteFile;
  34. bool _showVideo = false;
  35. bool _isLoadingVideoPlayer = false;
  36. VideoPlayerController? _videoPlayerController;
  37. ChewieController? _chewieController;
  38. @override
  39. void initState() {
  40. _enteFile = widget.enteFile;
  41. super.initState();
  42. }
  43. void _onLongPressEvent(bool isPressed) {
  44. if (_videoPlayerController != null && isPressed == false) {
  45. // stop playing video
  46. _videoPlayerController!.pause();
  47. }
  48. if (mounted) {
  49. setState(() {
  50. _showVideo = isPressed;
  51. });
  52. }
  53. }
  54. @override
  55. Widget build(BuildContext context) {
  56. Widget content;
  57. // check is long press is selected but videoPlayer is not configured yet
  58. if (_showVideo && _videoPlayerController == null) {
  59. _loadLiveVideo();
  60. }
  61. if (_showVideo && _videoPlayerController != null) {
  62. content = _getVideoPlayer();
  63. } else {
  64. content = ZoomableImage(
  65. _enteFile,
  66. tagPrefix: widget.tagPrefix,
  67. shouldDisableScroll: widget.shouldDisableScroll,
  68. backgroundDecoration: widget.backgroundDecoration,
  69. );
  70. }
  71. return GestureDetector(
  72. onLongPressStart: (_) => {_onLongPressEvent(true)},
  73. onLongPressEnd: (_) => {_onLongPressEvent(false)},
  74. child: content,
  75. );
  76. }
  77. @override
  78. void dispose() {
  79. if (_videoPlayerController != null) {
  80. _videoPlayerController!.pause();
  81. _videoPlayerController!.dispose();
  82. }
  83. if (_chewieController != null) {
  84. _chewieController!.dispose();
  85. }
  86. super.dispose();
  87. }
  88. Widget _getVideoPlayer() {
  89. _videoPlayerController!.seekTo(Duration.zero);
  90. _chewieController = ChewieController(
  91. videoPlayerController: _videoPlayerController!,
  92. aspectRatio: _videoPlayerController!.value.aspectRatio,
  93. autoPlay: true,
  94. autoInitialize: true,
  95. looping: true,
  96. allowFullScreen: false,
  97. showControls: false,
  98. );
  99. return Container(
  100. color: Colors.black,
  101. child: Chewie(controller: _chewieController!), // same for both theme
  102. );
  103. }
  104. Future<void> _loadLiveVideo() async {
  105. // do nothing is already loading or loaded
  106. if (_isLoadingVideoPlayer || _videoPlayerController != null) {
  107. return;
  108. }
  109. _isLoadingVideoPlayer = true;
  110. // For non-live photo, with fileType as Image, we still call _getMotionPhoto
  111. // to check if it is a motion photo. This is needed to handle earlier
  112. // uploads and upload from desktop
  113. final File? videoFile = _enteFile.isLivePhoto
  114. ? await _getLivePhotoVideo()
  115. : await _getMotionPhotoVideo();
  116. if (videoFile != null && videoFile.existsSync()) {
  117. _setVideoPlayerController(file: videoFile);
  118. } else if (_enteFile.isLivePhoto) {
  119. showShortToast(context, S.of(context).downloadFailed);
  120. }
  121. _isLoadingVideoPlayer = false;
  122. }
  123. Future<File?> _getLivePhotoVideo() async {
  124. if (_enteFile.isRemoteFile && !(await isFileCached(_enteFile, liveVideo: true))) {
  125. showShortToast(context, S.of(context).downloading);
  126. }
  127. File? videoFile = await getFile(widget.enteFile, liveVideo: true)
  128. .timeout(const Duration(seconds: 15))
  129. .onError((dynamic e, s) {
  130. _logger.info("getFile failed ${_enteFile.tag}", e);
  131. return null;
  132. });
  133. // FixMe: Here, we are fetching video directly when getFile failed
  134. // getFile with liveVideo as true can fail for file with localID when
  135. // the live photo was downloaded from remote.
  136. if ((videoFile == null || !videoFile.existsSync()) &&
  137. _enteFile.uploadedFileID != null) {
  138. videoFile = await getFileFromServer(widget.enteFile, liveVideo: true)
  139. .timeout(const Duration(seconds: 15))
  140. .onError((dynamic e, s) {
  141. _logger.info("getRemoteFile failed ${_enteFile.tag}", e);
  142. return null;
  143. });
  144. }
  145. return videoFile;
  146. }
  147. Future<File?> _getMotionPhotoVideo() async {
  148. if (_enteFile.isRemoteFile && !(await isFileCached(_enteFile))) {
  149. showShortToast(context, S.of(context).downloading);
  150. }
  151. final File? imageFile = await getFile(
  152. widget.enteFile,
  153. isOrigin: !Platform.isAndroid,
  154. ).timeout(const Duration(seconds: 15)).onError((dynamic e, s) {
  155. _logger.info("getFile failed ${_enteFile.tag}", e);
  156. return null;
  157. });
  158. if (imageFile != null) {
  159. final motionPhoto = MotionPhotos(imageFile.path);
  160. final index = await motionPhoto.getMotionVideoIndex();
  161. if (index != null) {
  162. // Update the metadata if it is not updated
  163. if (!_enteFile.isMotionPhoto && _enteFile.canEditMetaInfo) {
  164. FileMagicService.instance.updatePublicMagicMetadata(
  165. [_enteFile],
  166. {motionVideoIndexKey: index.start},
  167. ).ignore();
  168. }
  169. return motionPhoto.getMotionVideoFile(
  170. index: index,
  171. );
  172. }
  173. }
  174. return null;
  175. }
  176. VideoPlayerController _setVideoPlayerController({required File file}) {
  177. final videoPlayerController = VideoPlayerController.file(file);
  178. return _videoPlayerController = videoPlayerController
  179. ..initialize().whenComplete(() {
  180. if (mounted) {
  181. setState(() {
  182. _showVideo = true;
  183. });
  184. }
  185. });
  186. }
  187. }