zoomable_live_image.dart 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import 'dart:io' as io;
  2. import 'package:chewie/chewie.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:photos/core/constants.dart';
  6. import 'package:photos/models/file.dart';
  7. import 'package:photos/ui/viewer/file/zoomable_image.dart';
  8. import 'package:photos/utils/file_util.dart';
  9. import 'package:photos/utils/toast_util.dart';
  10. import 'package:shared_preferences/shared_preferences.dart';
  11. import 'package:video_player/video_player.dart';
  12. class ZoomableLiveImage extends StatefulWidget {
  13. final File file;
  14. final Function(bool)? shouldDisableScroll;
  15. final String? tagPrefix;
  16. final Decoration? backgroundDecoration;
  17. const ZoomableLiveImage(
  18. this.file, {
  19. Key? key,
  20. this.shouldDisableScroll,
  21. required this.tagPrefix,
  22. this.backgroundDecoration,
  23. }) : super(key: key);
  24. @override
  25. State<ZoomableLiveImage> createState() => _ZoomableLiveImageState();
  26. }
  27. class _ZoomableLiveImageState extends State<ZoomableLiveImage>
  28. with SingleTickerProviderStateMixin {
  29. final Logger _logger = Logger("ZoomableLiveImage");
  30. late File _file;
  31. bool _showVideo = false;
  32. bool _isLoadingVideoPlayer = false;
  33. VideoPlayerController? _videoPlayerController;
  34. ChewieController? _chewieController;
  35. @override
  36. void initState() {
  37. _file = widget.file;
  38. _showLivePhotoToast();
  39. super.initState();
  40. }
  41. void _onLongPressEvent(bool isPressed) {
  42. if (_videoPlayerController != null && isPressed == false) {
  43. // stop playing video
  44. _videoPlayerController!.pause();
  45. }
  46. if (mounted) {
  47. setState(() {
  48. _showVideo = isPressed;
  49. });
  50. }
  51. }
  52. @override
  53. Widget build(BuildContext context) {
  54. Widget content;
  55. // check is long press is selected but videoPlayer is not configured yet
  56. if (_showVideo && _videoPlayerController == null) {
  57. _loadLiveVideo();
  58. }
  59. if (_showVideo && _videoPlayerController != null) {
  60. content = _getVideoPlayer();
  61. } else {
  62. content = ZoomableImage(
  63. _file,
  64. tagPrefix: widget.tagPrefix,
  65. shouldDisableScroll: widget.shouldDisableScroll,
  66. backgroundDecoration: widget.backgroundDecoration,
  67. );
  68. }
  69. return GestureDetector(
  70. onLongPressStart: (_) => {_onLongPressEvent(true)},
  71. onLongPressEnd: (_) => {_onLongPressEvent(false)},
  72. child: content,
  73. );
  74. }
  75. @override
  76. void dispose() {
  77. if (_videoPlayerController != null) {
  78. _videoPlayerController!.pause();
  79. _videoPlayerController!.dispose();
  80. }
  81. if (_chewieController != null) {
  82. _chewieController!.dispose();
  83. }
  84. super.dispose();
  85. }
  86. Widget _getVideoPlayer() {
  87. _videoPlayerController!.seekTo(Duration.zero);
  88. _chewieController = ChewieController(
  89. videoPlayerController: _videoPlayerController!,
  90. aspectRatio: _videoPlayerController!.value.aspectRatio,
  91. autoPlay: true,
  92. autoInitialize: true,
  93. looping: true,
  94. allowFullScreen: false,
  95. showControls: false,
  96. );
  97. return Container(
  98. color: Colors.black,
  99. child: Chewie(controller: _chewieController!), // same for both theme
  100. );
  101. }
  102. Future<void> _loadLiveVideo() async {
  103. // do nothing is already loading or loaded
  104. if (_isLoadingVideoPlayer || _videoPlayerController != null) {
  105. return;
  106. }
  107. _isLoadingVideoPlayer = true;
  108. if (_file.isRemoteFile && !(await isFileCached(_file, liveVideo: true))) {
  109. showShortToast(context, "Downloading...");
  110. }
  111. io.File? videoFile = await getFile(widget.file, liveVideo: true)
  112. .timeout(const Duration(seconds: 15))
  113. .onError((dynamic e, s) {
  114. _logger.info("getFile failed ${_file.tag}", e);
  115. return null;
  116. });
  117. // FixMe: Here, we are fetching video directly when getFile failed
  118. // getFile with liveVideo as true can fail for file with localID when
  119. // the live photo was downloaded from remote.
  120. if ((videoFile == null || !videoFile.existsSync()) &&
  121. _file.uploadedFileID != null) {
  122. videoFile = await getFileFromServer(widget.file, liveVideo: true)
  123. .timeout(const Duration(seconds: 15))
  124. .onError((dynamic e, s) {
  125. _logger.info("getRemoteFile failed ${_file.tag}", e);
  126. return null;
  127. });
  128. }
  129. if (videoFile != null && videoFile.existsSync()) {
  130. _setVideoPlayerController(file: videoFile);
  131. } else {
  132. showShortToast(context, "Download failed");
  133. }
  134. _isLoadingVideoPlayer = false;
  135. }
  136. VideoPlayerController _setVideoPlayerController({required io.File file}) {
  137. final videoPlayerController = VideoPlayerController.file(file);
  138. return _videoPlayerController = videoPlayerController
  139. ..initialize().whenComplete(() {
  140. if (mounted) {
  141. setState(() {
  142. _showVideo = true;
  143. });
  144. }
  145. });
  146. }
  147. void _showLivePhotoToast() async {
  148. final preferences = await SharedPreferences.getInstance();
  149. final int promptTillNow = preferences.getInt(livePhotoToastCounterKey) ?? 0;
  150. if (promptTillNow < maxLivePhotoToastCount && mounted) {
  151. showToast(context, "Press and hold to play video");
  152. preferences.setInt(livePhotoToastCounterKey, promptTillNow + 1);
  153. }
  154. }
  155. }