1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

fix(mobile): Fix minor issues with downloading assets (#13609)

* improve download asset

* fix: download motion photos on ios

---------

Co-authored-by: dvbthien <dvbthien@gmail.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
dvbthien 2024-10-21 00:56:02 +07:00 committed by GitHub
parent 62e55f3db9
commit ee0130a58b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 119 additions and 57 deletions

View file

@ -10,6 +10,12 @@ abstract interface class IFileMediaRepository {
String? relativePath, String? relativePath,
}); });
Future<Asset?> saveImageWithFile(
String filePath, {
String? title,
String? relativePath,
});
Future<Asset?> saveVideo( Future<Asset?> saveVideo(
File file, { File file, {
required String title, required String title,

View file

@ -83,7 +83,7 @@ Future<void> initApp() async {
}; };
PlatformDispatcher.instance.onError = (error, stack) { PlatformDispatcher.instance.onError = (error, stack) {
debugPrint("FlutterError - Catch all: $error"); debugPrint("FlutterError - Catch all: $error \n $stack");
log.severe('PlatformDispatcher - Catch all', error, stack); log.severe('PlatformDispatcher - Catch all', error, stack);
return true; return true;
}; };

View file

@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/download/download_state.model.dart'; import 'package:immich_mobile/models/download/download_state.model.dart';
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/download.service.dart'; import 'package:immich_mobile/services/download.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/services/share.service.dart'; import 'package:immich_mobile/services/share.service.dart';
@ -15,10 +16,12 @@ import 'package:immich_mobile/widgets/common/share_dialog.dart';
class DownloadStateNotifier extends StateNotifier<DownloadState> { class DownloadStateNotifier extends StateNotifier<DownloadState> {
final DownloadService _downloadService; final DownloadService _downloadService;
final ShareService _shareService; final ShareService _shareService;
final AlbumService _albumService;
DownloadStateNotifier( DownloadStateNotifier(
this._downloadService, this._downloadService,
this._shareService, this._shareService,
this._albumService,
) : super( ) : super(
DownloadState( DownloadState(
downloadStatus: TaskStatus.complete, downloadStatus: TaskStatus.complete,
@ -76,7 +79,7 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
switch (update.status) { switch (update.status) {
case TaskStatus.complete: case TaskStatus.complete:
_downloadService.saveImage(update.task); _downloadService.saveImageWithPath(update.task);
_onDownloadComplete(update.task.taskId); _onDownloadComplete(update.task.taskId);
break; break;
@ -133,6 +136,7 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
showProgress: false, showProgress: false,
); );
} }
_albumService.refreshDeviceAlbums();
}); });
} }
@ -187,5 +191,6 @@ final downloadStateProvider =
((ref) => DownloadStateNotifier( ((ref) => DownloadStateNotifier(
ref.watch(downloadServiceProvider), ref.watch(downloadServiceProvider),
ref.watch(shareServiceProvider), ref.watch(shareServiceProvider),
ref.watch(albumServiceProvider),
)), )),
); );

View file

@ -25,6 +25,20 @@ class FileMediaRepository implements IFileMediaRepository {
return AssetMediaRepository.toAsset(entity); return AssetMediaRepository.toAsset(entity);
} }
@override
Future<Asset?> saveImageWithFile(
String filePath, {
String? title,
String? relativePath,
}) async {
final entity = await PhotoManager.editor.saveImageWithPath(
filePath,
title: title,
relativePath: relativePath,
);
return AssetMediaRepository.toAsset(entity);
}
@override @override
Future<Asset?> saveLivePhoto({ Future<Asset?> saveLivePhoto({
required File image, required File image,

View file

@ -1,7 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
@ -12,6 +12,7 @@ import 'package:immich_mobile/repositories/download.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/download.dart'; import 'package:immich_mobile/utils/download.dart';
import 'package:logging/logging.dart';
final downloadServiceProvider = Provider( final downloadServiceProvider = Provider(
(ref) => DownloadService( (ref) => DownloadService(
@ -23,6 +24,7 @@ final downloadServiceProvider = Provider(
class DownloadService { class DownloadService {
final IDownloadRepository _downloadRepository; final IDownloadRepository _downloadRepository;
final IFileMediaRepository _fileMediaRepository; final IFileMediaRepository _fileMediaRepository;
final Logger _log = Logger("DownloadService");
void Function(TaskStatusUpdate)? onImageDownloadStatus; void Function(TaskStatusUpdate)? onImageDownloadStatus;
void Function(TaskStatusUpdate)? onVideoDownloadStatus; void Function(TaskStatusUpdate)? onVideoDownloadStatus;
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus; void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
@ -55,19 +57,25 @@ class DownloadService {
onLivePhotoDownloadStatus?.call(update); onLivePhotoDownloadStatus?.call(update);
} }
Future<bool> saveImage(Task task) async { Future<bool> saveImageWithPath(Task task) async {
final filePath = await task.filePath(); final filePath = await task.filePath();
final title = task.filename; final title = task.filename;
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
final data = await File(filePath).readAsBytes(); try {
final Asset? resultAsset = await _fileMediaRepository.saveImageWithFile(
final Asset? resultAsset = await _fileMediaRepository.saveImage( filePath,
data, title: title,
title: title, relativePath: relativePath,
relativePath: relativePath, );
); return resultAsset != null;
} catch (error, stack) {
return resultAsset != null; _log.severe("Error saving image", error, stack);
return false;
} finally {
if (await File(filePath).exists()) {
await File(filePath).delete();
}
}
} }
Future<bool> saveVideo(Task task) async { Future<bool> saveVideo(Task task) async {
@ -75,58 +83,74 @@ class DownloadService {
final title = task.filename; final title = task.filename;
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
final file = File(filePath); final file = File(filePath);
try {
final Asset? resultAsset = await _fileMediaRepository.saveVideo( final Asset? resultAsset = await _fileMediaRepository.saveVideo(
file, file,
title: title, title: title,
relativePath: relativePath, relativePath: relativePath,
); );
return resultAsset != null;
return resultAsset != null; } catch (error, stack) {
_log.severe("Error saving video", error, stack);
return false;
} finally {
if (await file.exists()) {
await file.delete();
}
}
} }
Future<bool> saveLivePhotos( Future<bool> saveLivePhotos(
Task task, Task task,
String livePhotosId, String livePhotosId,
) async { ) async {
final records = await _downloadRepository.getLiveVideoTasks();
if (records.length < 2) {
return false;
}
final imageRecord =
_findTaskRecord(records, livePhotosId, LivePhotosPart.image);
final videoRecord =
_findTaskRecord(records, livePhotosId, LivePhotosPart.video);
final imageFilePath = await imageRecord.task.filePath();
final videoFilePath = await videoRecord.task.filePath();
try { try {
final records = await _downloadRepository.getLiveVideoTasks(); final result = await _fileMediaRepository.saveLivePhoto(
if (records.length < 2) {
return false;
}
final imageRecord = records.firstWhere(
(record) {
final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
return metadata.id == livePhotosId &&
metadata.part == LivePhotosPart.image;
},
);
final videoRecord = records.firstWhere((record) {
final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
return metadata.id == livePhotosId &&
metadata.part == LivePhotosPart.video;
});
final imageFilePath = await imageRecord.task.filePath();
final videoFilePath = await videoRecord.task.filePath();
final resultAsset = await _fileMediaRepository.saveLivePhoto(
image: File(imageFilePath), image: File(imageFilePath),
video: File(videoFilePath), video: File(videoFilePath),
title: task.filename, title: task.filename,
); );
return result != null;
} on PlatformException catch (error, stack) {
// Handle saving MotionPhotos on iOS
if (error.code == 'PHPhotosErrorDomain (-1)') {
final result = await _fileMediaRepository
.saveImageWithFile(imageFilePath, title: task.filename);
return result != null;
}
_log.severe("Error saving live photo", error, stack);
return false;
} catch (error, stack) {
_log.severe("Error saving live photo", error, stack);
return false;
} finally {
final imageFile = File(imageFilePath);
if (await imageFile.exists()) {
await imageFile.delete();
}
final videoFile = File(videoFilePath);
if (await videoFile.exists()) {
await videoFile.delete();
}
await _downloadRepository.deleteRecordsWithIds([ await _downloadRepository.deleteRecordsWithIds([
imageRecord.task.taskId, imageRecord.task.taskId,
videoRecord.task.taskId, videoRecord.task.taskId,
]); ]);
return resultAsset != null;
} catch (error) {
debugPrint("Error saving live photo: $error");
return false;
} }
} }
@ -151,7 +175,9 @@ class DownloadService {
await _downloadRepository.download( await _downloadRepository.download(
_buildDownloadTask( _buildDownloadTask(
asset.livePhotoVideoId!, asset.livePhotoVideoId!,
asset.fileName.toUpperCase().replaceAll(".HEIC", '.MOV'), asset.fileName
.toUpperCase()
.replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'),
group: downloadGroupLivePhoto, group: downloadGroupLivePhoto,
metadata: LivePhotosMetadata( metadata: LivePhotosMetadata(
part: LivePhotosPart.video, part: LivePhotosPart.video,
@ -191,3 +217,14 @@ class DownloadService {
); );
} }
} }
TaskRecord _findTaskRecord(
List<TaskRecord> records,
String livePhotosId,
LivePhotosPart part,
) {
return records.firstWhere((record) {
final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
return metadata.id == livePhotosId && metadata.part == part;
});
}

View file

@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget {
populateTestLoginInfo1() { populateTestLoginInfo1() {
usernameController.text = 'testuser@email.com'; usernameController.text = 'testuser@email.com';
passwordController.text = 'password'; passwordController.text = 'password';
serverEndpointController.text = 'http://192.168.1.118:2283/api'; serverEndpointController.text = 'http://10.1.15.216:2283/api';
} }
login() async { login() async {

View file

@ -1211,18 +1211,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: photo_manager name: photo_manager
sha256: "32a1ce1095aeaaa792a29f28c1f74613aa75109f21c2d4ab85be3ad9964230a4" sha256: "70159eee32203e8162d49d588232f0299ed3f383c63eef1e899cb6b83dee6b26"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.5.0" version: "3.5.1"
photo_manager_image_provider: photo_manager_image_provider:
dependency: "direct main" dependency: "direct main"
description: description:
name: photo_manager_image_provider name: photo_manager_image_provider
sha256: "38ef1023dc11de3a8669f16e7c981673b3c5cfee715d17120f4b87daa2cdd0af" sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.2.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:

View file

@ -13,8 +13,8 @@ dependencies:
sdk: flutter sdk: flutter
path_provider_ios: path_provider_ios:
photo_manager: ^3.5.0 photo_manager: ^3.5.1
photo_manager_image_provider: ^2.1.1 photo_manager_image_provider: ^2.2.0
flutter_hooks: ^0.20.4 flutter_hooks: ^0.20.4
hooks_riverpod: ^2.4.9 hooks_riverpod: ^2.4.9
riverpod_annotation: ^2.3.3 riverpod_annotation: ^2.3.3