diff --git a/mobile/lib/interfaces/file_media.interface.dart b/mobile/lib/interfaces/file_media.interface.dart index c898183d792..ea01819dc3d 100644 --- a/mobile/lib/interfaces/file_media.interface.dart +++ b/mobile/lib/interfaces/file_media.interface.dart @@ -10,6 +10,12 @@ abstract interface class IFileMediaRepository { String? relativePath, }); + Future saveImageWithFile( + String filePath, { + String? title, + String? relativePath, + }); + Future saveVideo( File file, { required String title, diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 40eda30204e..48bc936a827 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -83,7 +83,7 @@ Future initApp() async { }; PlatformDispatcher.instance.onError = (error, stack) { - debugPrint("FlutterError - Catch all: $error"); + debugPrint("FlutterError - Catch all: $error \n $stack"); log.severe('PlatformDispatcher - Catch all', error, stack); return true; }; diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index d4aa2823b5b..68b120c38a7 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.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/livephotos_medatada.model.dart'; +import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/download.service.dart'; import 'package:immich_mobile/entities/asset.entity.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 { final DownloadService _downloadService; final ShareService _shareService; + final AlbumService _albumService; DownloadStateNotifier( this._downloadService, this._shareService, + this._albumService, ) : super( DownloadState( downloadStatus: TaskStatus.complete, @@ -76,7 +79,7 @@ class DownloadStateNotifier extends StateNotifier { switch (update.status) { case TaskStatus.complete: - _downloadService.saveImage(update.task); + _downloadService.saveImageWithPath(update.task); _onDownloadComplete(update.task.taskId); break; @@ -133,6 +136,7 @@ class DownloadStateNotifier extends StateNotifier { showProgress: false, ); } + _albumService.refreshDeviceAlbums(); }); } @@ -187,5 +191,6 @@ final downloadStateProvider = ((ref) => DownloadStateNotifier( ref.watch(downloadServiceProvider), ref.watch(shareServiceProvider), + ref.watch(albumServiceProvider), )), ); diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index 5612b378c3c..15f7a51e150 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -25,6 +25,20 @@ class FileMediaRepository implements IFileMediaRepository { return AssetMediaRepository.toAsset(entity); } + @override + Future saveImageWithFile( + String filePath, { + String? title, + String? relativePath, + }) async { + final entity = await PhotoManager.editor.saveImageWithPath( + filePath, + title: title, + relativePath: relativePath, + ); + return AssetMediaRepository.toAsset(entity); + } + @override Future saveLivePhoto({ required File image, diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index 996cbe61f19..7cf6f309e98 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -1,7 +1,7 @@ import 'dart:io'; 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:immich_mobile/entities/store.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/services/api.service.dart'; import 'package:immich_mobile/utils/download.dart'; +import 'package:logging/logging.dart'; final downloadServiceProvider = Provider( (ref) => DownloadService( @@ -23,6 +24,7 @@ final downloadServiceProvider = Provider( class DownloadService { final IDownloadRepository _downloadRepository; final IFileMediaRepository _fileMediaRepository; + final Logger _log = Logger("DownloadService"); void Function(TaskStatusUpdate)? onImageDownloadStatus; void Function(TaskStatusUpdate)? onVideoDownloadStatus; void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus; @@ -55,19 +57,25 @@ class DownloadService { onLivePhotoDownloadStatus?.call(update); } - Future saveImage(Task task) async { + Future saveImageWithPath(Task task) async { final filePath = await task.filePath(); final title = task.filename; final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; - final data = await File(filePath).readAsBytes(); - - final Asset? resultAsset = await _fileMediaRepository.saveImage( - data, - title: title, - relativePath: relativePath, - ); - - return resultAsset != null; + try { + final Asset? resultAsset = await _fileMediaRepository.saveImageWithFile( + filePath, + title: title, + relativePath: relativePath, + ); + return resultAsset != null; + } catch (error, stack) { + _log.severe("Error saving image", error, stack); + return false; + } finally { + if (await File(filePath).exists()) { + await File(filePath).delete(); + } + } } Future saveVideo(Task task) async { @@ -75,58 +83,74 @@ class DownloadService { final title = task.filename; final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; final file = File(filePath); - - final Asset? resultAsset = await _fileMediaRepository.saveVideo( - file, - title: title, - relativePath: relativePath, - ); - - return resultAsset != null; + try { + final Asset? resultAsset = await _fileMediaRepository.saveVideo( + file, + title: title, + relativePath: relativePath, + ); + return resultAsset != null; + } catch (error, stack) { + _log.severe("Error saving video", error, stack); + return false; + } finally { + if (await file.exists()) { + await file.delete(); + } + } } Future saveLivePhotos( Task task, String livePhotosId, ) 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 { - final records = await _downloadRepository.getLiveVideoTasks(); - 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( + final result = await _fileMediaRepository.saveLivePhoto( image: File(imageFilePath), video: File(videoFilePath), 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([ imageRecord.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( _buildDownloadTask( asset.livePhotoVideoId!, - asset.fileName.toUpperCase().replaceAll(".HEIC", '.MOV'), + asset.fileName + .toUpperCase() + .replaceAll(RegExp(r"\.(JPG|HEIC)$"), '.MOV'), group: downloadGroupLivePhoto, metadata: LivePhotosMetadata( part: LivePhotosPart.video, @@ -191,3 +217,14 @@ class DownloadService { ); } } + +TaskRecord _findTaskRecord( + List records, + String livePhotosId, + LivePhotosPart part, +) { + return records.firstWhere((record) { + final metadata = LivePhotosMetadata.fromJson(record.task.metaData); + return metadata.id == livePhotosId && metadata.part == part; + }); +} diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 46e86718583..51383fe1950 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget { populateTestLoginInfo1() { usernameController.text = 'testuser@email.com'; passwordController.text = 'password'; - serverEndpointController.text = 'http://192.168.1.118:2283/api'; + serverEndpointController.text = 'http://10.1.15.216:2283/api'; } login() async { diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 1f29bf7830c..fe40017ba85 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1211,18 +1211,18 @@ packages: dependency: "direct main" description: name: photo_manager - sha256: "32a1ce1095aeaaa792a29f28c1f74613aa75109f21c2d4ab85be3ad9964230a4" + sha256: "70159eee32203e8162d49d588232f0299ed3f383c63eef1e899cb6b83dee6b26" url: "https://pub.dev" source: hosted - version: "3.5.0" + version: "3.5.1" photo_manager_image_provider: dependency: "direct main" description: name: photo_manager_image_provider - sha256: "38ef1023dc11de3a8669f16e7c981673b3c5cfee715d17120f4b87daa2cdd0af" + sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" platform: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 703736e3d96..5177e64bcfe 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -13,8 +13,8 @@ dependencies: sdk: flutter path_provider_ios: - photo_manager: ^3.5.0 - photo_manager_image_provider: ^2.1.1 + photo_manager: ^3.5.1 + photo_manager_image_provider: ^2.2.0 flutter_hooks: ^0.20.4 hooks_riverpod: ^2.4.9 riverpod_annotation: ^2.3.3