ente/mobile/lib/ui/viewer/file/zoomable_live_image_new.dart
2024-03-01 12:25:37 +05:30

213 lines
6.6 KiB
Dart

import "dart:io";
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import "package:media_kit/media_kit.dart";
import "package:media_kit_video/media_kit_video.dart";
import 'package:motion_photos/motion_photos.dart';
import "package:photos/generated/l10n.dart";
import "package:photos/models/file/extensions/file_props.dart";
import 'package:photos/models/file/file.dart';
import "package:photos/models/metadata/file_magic.dart";
import "package:photos/services/file_magic_service.dart";
import "package:photos/services/local_file_update_service.dart";
import 'package:photos/ui/viewer/file/zoomable_image.dart';
import 'package:photos/utils/file_util.dart';
import 'package:photos/utils/toast_util.dart';
class ZoomableLiveImageNew extends StatefulWidget {
final EnteFile enteFile;
final Function(bool)? shouldDisableScroll;
final String? tagPrefix;
final Decoration? backgroundDecoration;
const ZoomableLiveImageNew(
this.enteFile, {
Key? key,
this.shouldDisableScroll,
required this.tagPrefix,
this.backgroundDecoration,
}) : super(key: key);
@override
State<ZoomableLiveImageNew> createState() => _ZoomableLiveImageNewState();
}
class _ZoomableLiveImageNewState extends State<ZoomableLiveImageNew>
with SingleTickerProviderStateMixin {
final Logger _logger = Logger("ZoomableLiveImageNew");
late EnteFile _enteFile;
bool _showVideo = false;
bool _isLoadingVideoPlayer = false;
late final _player = Player();
VideoController? _videoController;
@override
void initState() {
_enteFile = widget.enteFile;
_logger.info(
'initState for ${_enteFile.generatedID} with tag ${_enteFile.tag} and name ${_enteFile.displayName}',
);
if (_enteFile.isLivePhoto && _enteFile.isUploaded) {
LocalFileUpdateService.instance.checkLivePhoto(_enteFile).ignore();
}
super.initState();
}
void _onLongPressEvent(bool isPressed) {
if (_videoController != null && isPressed == false) {
// stop playing video
_videoController!.player.pause();
}
if (mounted) {
setState(() {
_showVideo = isPressed;
});
}
}
@override
Widget build(BuildContext context) {
Widget content;
// check is long press is selected but videoPlayer is not configured yet
if (_showVideo && _videoController == null) {
_loadLiveVideo();
}
if (_showVideo && _videoController != null) {
content = _getVideoPlayer();
} else {
content = ZoomableImage(
_enteFile,
tagPrefix: widget.tagPrefix,
shouldDisableScroll: widget.shouldDisableScroll,
backgroundDecoration: widget.backgroundDecoration,
);
}
return GestureDetector(
onLongPressStart: (_) => {_onLongPressEvent(true)},
onLongPressEnd: (_) => {_onLongPressEvent(false)},
child: content,
);
}
@override
void dispose() {
if (_videoController != null) {
_videoController!.player.stop();
_videoController!.player.dispose();
}
super.dispose();
}
Widget _getVideoPlayer() {
_videoController!.player.seek(Duration.zero);
_videoController!.player.setPlaylistMode(PlaylistMode.single);
_videoController!.player.play();
return Container(
color: Colors.black,
child: Video(
controller: _videoController!,
controls: null,
),
);
}
Future<void> _loadLiveVideo() async {
// do nothing is already loading or loaded
if (_isLoadingVideoPlayer || _videoController != null) {
return;
}
_isLoadingVideoPlayer = true;
// For non-live photo, with fileType as Image, we still call _getMotionPhoto
// to check if it is a motion photo. This is needed to handle earlier
// uploads and upload from desktop
final File? videoFile = _enteFile.isLivePhoto
? await _getLivePhotoVideo()
: await _getMotionPhotoVideo();
if (videoFile != null && videoFile.existsSync()) {
_setVideoController(videoFile.path);
} else if (_enteFile.isLivePhoto) {
showShortToast(context, S.of(context).downloadFailed);
}
_isLoadingVideoPlayer = false;
}
Future<File?> _getLivePhotoVideo() async {
if (_enteFile.isRemoteFile &&
!(await isFileCached(_enteFile, liveVideo: true))) {
showShortToast(context, S.of(context).downloading);
}
File? videoFile = await getFile(widget.enteFile, liveVideo: true)
.timeout(const Duration(seconds: 15))
.onError((dynamic e, s) {
_logger.info("getFile failed ${_enteFile.tag}", e);
return null;
});
// FixMe: Here, we are fetching video directly when getFile failed
// getFile with liveVideo as true can fail for file with localID when
// the live photo was downloaded from remote.
if ((videoFile == null || !videoFile.existsSync()) &&
_enteFile.uploadedFileID != null) {
videoFile = await getFileFromServer(widget.enteFile, liveVideo: true)
.timeout(const Duration(seconds: 15))
.onError((dynamic e, s) {
_logger.info("getRemoteFile failed ${_enteFile.tag}", e);
return null;
});
}
return videoFile;
}
Future<File?> _getMotionPhotoVideo() async {
if (_enteFile.isRemoteFile && !(await isFileCached(_enteFile))) {
showShortToast(context, S.of(context).downloading);
}
final File? imageFile = await getFile(
widget.enteFile,
isOrigin: !Platform.isAndroid,
).timeout(const Duration(seconds: 15)).onError((dynamic e, s) {
_logger.info("getFile failed ${_enteFile.tag}", e);
return null;
});
if (imageFile != null) {
final motionPhoto = MotionPhotos(imageFile.path);
final index = await motionPhoto.getMotionVideoIndex();
if (index != null) {
// Update the metadata if it is not updated
if (!_enteFile.isMotionPhoto && _enteFile.canEditMetaInfo) {
FileMagicService.instance.updatePublicMagicMetadata(
[_enteFile],
{motionVideoIndexKey: index.start},
).ignore();
}
return motionPhoto.getMotionVideoFile(
index: index,
);
} else if (_enteFile.isMotionPhoto && _enteFile.canEditMetaInfo) {
_logger.finest('Incorrectly tagged as MP, reset tag ${_enteFile.tag}');
FileMagicService.instance.updatePublicMagicMetadata(
[_enteFile],
{motionVideoIndexKey: 0},
).ignore();
}
}
return null;
}
void _setVideoController(String url) {
if (mounted) {
setState(() {
_videoController = VideoController(_player);
_player.open(Media(url));
_showVideo = true;
});
}
}
}