zoomable_live_image.dart 6.9 KB

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