fix(mobile): live / motion photo download (#5607)

* reverts: 5566

* fix: stitch livePhoto only in iOS

* fix: PMProgressHandler only on iOS

* ios: fallback to saving image if livephoto fails

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong 2023-12-10 15:56:39 +00:00 committed by GitHub
parent 33529d1d9b
commit 960b68b02f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 34 additions and 16 deletions

View File

@ -19,9 +19,11 @@ class ImageViewerService {
ImageViewerService(this._apiService); ImageViewerService(this._apiService);
Future<bool> downloadAssetToDevice(Asset asset) async { Future<bool> downloadAssetToDevice(Asset asset) async {
File? imageFile;
File? videoFile;
try { try {
// Download LivePhotos image and motion part // Download LivePhotos image and motion part
if (asset.isImage && asset.livePhotoVideoId != null) { if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo( var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.remoteId!, asset.remoteId!,
); );
@ -40,11 +42,11 @@ class ImageViewerService {
return false; return false;
} }
final AssetEntity? entity; AssetEntity? entity;
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
File videoFile = await File('${tempDir.path}/livephoto.mov').create(); videoFile = await File('${tempDir.path}/livephoto.mov').create();
File imageFile = await File('${tempDir.path}/livephoto.heic').create(); imageFile = await File('${tempDir.path}/livephoto.heic').create();
videoFile.writeAsBytesSync(motionReponse.bodyBytes); videoFile.writeAsBytesSync(motionReponse.bodyBytes);
imageFile.writeAsBytesSync(imageResponse.bodyBytes); imageFile.writeAsBytesSync(imageResponse.bodyBytes);
@ -54,6 +56,17 @@ class ImageViewerService {
title: asset.fileName, title: asset.fileName,
); );
if (entity == null) {
_log.warning(
"Asset cannot be saved as a live photo. This is most likely a motion photo. Saving only the image file",
);
entity = await PhotoManager.editor.saveImage(
imageResponse.bodyBytes,
title: asset.fileName,
);
}
return entity != null; return entity != null;
} else { } else {
var res = await _apiService.assetApi var res = await _apiService.assetApi
@ -75,17 +88,20 @@ class ImageViewerService {
); );
} else { } else {
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
File tempFile = videoFile = await File('${tempDir.path}/${asset.fileName}').create();
await File('${tempDir.path}/${asset.fileName}').create(); videoFile.writeAsBytesSync(res.bodyBytes);
tempFile.writeAsBytesSync(res.bodyBytes);
entity = await PhotoManager.editor entity = await PhotoManager.editor
.saveVideo(tempFile, title: asset.fileName); .saveVideo(videoFile, title: asset.fileName);
} }
return entity != null; return entity != null;
} }
} catch (error, stack) { } catch (error, stack) {
_log.severe("Error saving file ${error.toString()}", error, stack); _log.severe("Error saving file ${error.toString()}", error, stack);
return false; return false;
} finally {
// Clear temp files
imageFile?.delete();
videoFile?.delete();
} }
} }
} }

View File

@ -453,7 +453,7 @@ class BackgroundService {
); );
_cancellationToken = CancellationToken(); _cancellationToken = CancellationToken();
final pmProgressHandler = PMProgressHandler(); final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
final bool ok = await backupService.backupAsset( final bool ok = await backupService.backupAsset(
toUpload, toUpload,

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -447,9 +449,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// Perform Backup // Perform Backup
state = state.copyWith(cancelToken: CancellationToken()); state = state.copyWith(cancelToken: CancellationToken());
final pmProgressHandler = PMProgressHandler(); final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
pmProgressHandler.stream.listen((event) { pmProgressHandler?.stream.listen((event) {
final double progress = event.progress; final double progress = event.progress;
state = state.copyWith(iCloudDownloadProgress: progress); state = state.copyWith(iCloudDownloadProgress: progress);
}); });

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -208,7 +210,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
state.totalAssetsToUpload == 1; state.totalAssetsToUpload == 1;
state = state =
state.copyWith(showDetailedNotification: showDetailedNotification); state.copyWith(showDetailedNotification: showDetailedNotification);
final pmProgressHandler = PMProgressHandler(); final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
final bool ok = await ref.read(backupServiceProvider).backupAsset( final bool ok = await ref.read(backupServiceProvider).backupAsset(
allUploadAssets, allUploadAssets,

View File

@ -206,7 +206,7 @@ class BackupService {
Future<bool> backupAsset( Future<bool> backupAsset(
Iterable<AssetEntity> assetList, Iterable<AssetEntity> assetList,
http.CancellationToken cancelToken, http.CancellationToken cancelToken,
PMProgressHandler pmProgressHandler, PMProgressHandler? pmProgressHandler,
Function(String, String, bool) uploadSuccessCb, Function(String, String, bool) uploadSuccessCb,
Function(int, int) uploadProgressCb, Function(int, int) uploadProgressCb,
Function(CurrentUploadAsset) setCurrentUploadAssetCb, Function(CurrentUploadAsset) setCurrentUploadAssetCb,

View File

@ -3,7 +3,6 @@ import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/models/user.dart';
import 'package:immich_mobile/shared/ui/transparent_image.dart'; import 'package:immich_mobile/shared/ui/transparent_image.dart';
@ -35,11 +34,10 @@ class UserCircleAvatar extends ConsumerWidget {
color: isDarkTheme && user.avatarColor == AvatarColorEnum.primary color: isDarkTheme && user.avatarColor == AvatarColorEnum.primary
? Colors.black ? Colors.black
: Colors.white, : Colors.white,
backgroundColor: user.avatarColor.toColor(),
), ),
); );
return CircleAvatar( return CircleAvatar(
backgroundColor: context.primaryColor, backgroundColor: user.avatarColor.toColor(),
radius: radius, radius: radius,
child: user.profileImagePath.isEmpty child: user.profileImagePath.isEmpty
? textIcon ? textIcon