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:
parent
62e55f3db9
commit
ee0130a58b
8 changed files with 119 additions and 57 deletions
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue