diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cc665f5..36fa6f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,13 @@ To know more about breaking changes, see the [Migration Guide][]. ## Unreleased -*None.* +### Features + +- Allows to get the duration of a Live Photo with `AssetEntity.durationWithOptions` on iOS and macOS. + +### Improvements + +- Improves the options when fetching fixed number of assets on iOS and macOS. ## 3.5.2 diff --git a/example/lib/page/image_list_page.dart b/example/lib/page/image_list_page.dart index 1e781d50..c5ace308 100644 --- a/example/lib/page/image_list_page.dart +++ b/example/lib/page/image_list_page.dart @@ -164,11 +164,16 @@ class _GalleryContentListPageState extends State { child: const Text('Get file'), onPressed: () => getFile(entity), ), - if (entity.type == AssetType.video || entity.isLivePhoto) + if (entity.isLivePhoto) ElevatedButton( child: const Text('Get MP4 file'), onPressed: () => getFileWithMP4(entity), ), + if (entity.isLivePhoto) + ElevatedButton( + child: const Text('Get Live Photo duration'), + onPressed: () => getDurationOfLivePhoto(entity), + ), ElevatedButton( child: const Text('Show detail page'), onPressed: () => routeToDetailPage(entity), @@ -266,6 +271,11 @@ class _GalleryContentListPageState extends State { print(file); } + Future getDurationOfLivePhoto(AssetEntity entity) async { + final duration = await entity.durationWithOptions(withSubtype: true); + print(duration); + } + Future routeToDetailPage(AssetEntity entity) async { Navigator.of(context).push( MaterialPageRoute(builder: (_) => DetailPage(entity: entity)), diff --git a/ios/Classes/PMPlugin.m b/ios/Classes/PMPlugin.m index 67f1f9b3..522d724b 100644 --- a/ios/Classes/PMPlugin.m +++ b/ios/Classes/PMPlugin.m @@ -522,6 +522,10 @@ - (void)handleMethodResultHandler:(ResultHandler *)handler manager:(PMManager *) subtype:subtype fileType:fileType]; [handler reply:@(exists)]; + } else if ([call.method isEqualToString:@"getDurationWithOptions"]) { + NSString *assetId = call.arguments[@"id"]; + int subtype = [call.arguments[@"subtype"] intValue]; + [manager getDurationWithOptions:assetId subtype:subtype resultHandler:handler]; } else if ([call.method isEqualToString:@"getTitleAsync"]) { NSString *assetId = call.arguments[@"id"]; int subtype = [call.arguments[@"subtype"] intValue]; diff --git a/ios/Classes/core/PHAsset+PM_COMMON.m b/ios/Classes/core/PHAsset+PM_COMMON.m index 315892b5..eeebfd26 100644 --- a/ios/Classes/core/PHAsset+PM_COMMON.m +++ b/ios/Classes/core/PHAsset+PM_COMMON.m @@ -34,6 +34,9 @@ - (bool)isLivePhoto { if (@available(iOS 9.1, *)) { return (self.mediaSubtypes & PHAssetMediaSubtypePhotoLive) == PHAssetMediaSubtypePhotoLive; } + if (@available(macOS 14.0, *)) { + return (self.mediaSubtypes & PHAssetMediaSubtypePhotoLive) == PHAssetMediaSubtypePhotoLive; + } return NO; } diff --git a/ios/Classes/core/PMManager.h b/ios/Classes/core/PMManager.h index d5f20ceb..ae9021e2 100644 --- a/ios/Classes/core/PMManager.h +++ b/ios/Classes/core/PMManager.h @@ -88,6 +88,10 @@ typedef void (^AssetBlockResult)(PMAssetEntity *, NSObject *); subtype:(int)subtype fileType:(AVFileType)fileType; +- (void)getDurationWithOptions:(NSString *)assetId + subtype:(int)subtype + resultHandler:(NSObject *)handler; + - (NSString*)getTitleAsyncWithAssetId:(NSString *)assetId subtype:(int)subtype isOrigin:(BOOL)isOrigin @@ -101,13 +105,13 @@ typedef void (^AssetBlockResult)(PMAssetEntity *, NSObject *); - (NSArray *)getSubPathWithId:(NSString *)id type:(int)type albumType:(int)albumType option:(NSObject *)option; -- (void)copyAssetWithId:(NSString *)id toGallery:(NSString *)gallery block:(void (^)(PMAssetEntity *entity, NSObject *msg))block; +- (void)copyAssetWithId:(NSString *)id toGallery:(NSString *)gallery block:(AssetBlockResult)block; - (void)createFolderWithName:(NSString *)name parentId:(NSString *)id block:(void (^)(NSString *newId, NSObject *error))block; - (void)createAlbumWithName:(NSString *)name parentId:(NSString *)id block:(void (^)(NSString *newId, NSObject *error))block; -- (void)removeInAlbumWithAssetId:(NSArray *)id albumId:(NSString *)albumId block:(void (^)(NSObject *error))block; +- (void)removeInAlbumWithAssetId:(NSArray *)ids albumId:(NSString *)albumId block:(void (^)(NSObject *error))block; - (void)removeCollectionWithId:(NSString *)id type:(int)type block:(void (^)(NSObject *))block; @@ -115,7 +119,7 @@ typedef void (^AssetBlockResult)(PMAssetEntity *, NSObject *); - (void)clearFileCache; -- (void)requestCacheAssetsThumb:(NSArray *)identifiers option:(PMThumbLoadOption *)option; +- (void)requestCacheAssetsThumb:(NSArray *)ids option:(PMThumbLoadOption *)option; - (void)cancelCacheRequests; diff --git a/ios/Classes/core/PMManager.m b/ios/Classes/core/PMManager.m index fe392cc4..600f5ba0 100644 --- a/ios/Classes/core/PMManager.m +++ b/ios/Classes/core/PMManager.m @@ -33,6 +33,12 @@ - (PHCachingImageManager *)cachingManager { return __cachingManager; } +- (PHFetchOptions *)singleFetchOptions { + PHFetchOptions *options = [PHFetchOptions new]; + options.fetchLimit = 1; + return options; +} + - (NSArray *)getAssetPathList:(int)type hasAll:(BOOL)hasAll onlyAll:(BOOL)onlyAll option:(NSObject *)option pathFilterOption:(PMPathFilterOption *)pathFilterOption { NSMutableArray *array = [NSMutableArray new]; PHFetchOptions *assetOptions = [self getAssetOptions:type filterOption:option]; @@ -147,9 +153,8 @@ - (NSUInteger)getAssetCountWithType:(int)type option:(NSObject *)f } - (BOOL)existsWithId:(NSString *)assetId { - PHFetchResult *result = - [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[PHFetchOptions new]]; - return result && result.count > 0; + PHFetchResult *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]]; + return result && result.count == 1; } - (BOOL)entityIsLocallyAvailable:(NSString *)assetId @@ -157,7 +162,7 @@ - (BOOL)entityIsLocallyAvailable:(NSString *)assetId isOrigin:(BOOL)isOrigin subtype:(int)subtype fileType:(AVFileType)fileType { - PHFetchResult *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[PHFetchOptions new]]; + PHFetchResult *result = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]]; if (!result) { return NO; } @@ -403,13 +408,11 @@ - (PMAssetEntity *)getAssetEntity:(NSString *)assetId withCache:(BOOL)withCache return entity; } } - PHFetchResult *result = - [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:nil]; - if (result == nil || result.count == 0) { + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]]; + PHAsset *asset = [self getFirstObjFromFetchResult:fetchResult]; + if (!asset) { return nil; } - - PHAsset *asset = result[0]; entity = [self convertPHAssetToAssetEntity:asset needTitle:NO]; [cacheContainer putAssetEntity:entity]; return entity; @@ -895,7 +898,7 @@ - (NSString *)makeAssetOutputPath:(PHAsset *)asset if (resource) { filename = resource.originalFilename; } else { - filename = [asset valueForKey:@"filename"]; + filename = [asset title]; } filename = [NSString stringWithFormat:@"%@_%@%@_%@", id, modifiedDate, isOrigin ? @"_o" : @"", filename]; @@ -1139,9 +1142,9 @@ + (void)openSetting:(NSObject*)result { - (void)deleteWithIds:(NSArray *)ids changedBlock:(ChangeIds)block { [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ - PHFetchResult *result = - [PHAsset fetchAssetsWithLocalIdentifiers:ids - options:[PHFetchOptions new]]; + PHFetchOptions *options = [PHFetchOptions new]; + options.fetchLimit = ids.count; + PHFetchResult *result = [PHAsset fetchAssetsWithLocalIdentifiers:ids options:options]; [PHAssetChangeRequest deleteAssets:result]; } completionHandler:^(BOOL success, NSError *error) { @@ -1281,11 +1284,50 @@ - (void)saveLivePhoto:(NSString *)imagePath }]; } +- (void)getDurationWithOptions:(NSString *)assetId + subtype:(int)subtype + resultHandler:(NSObject *)handler { + PMAssetEntity *entity = [self getAssetEntity:assetId]; + if (!entity) { + [handler replyError:@"Not exists."]; + return; + } + PHAsset *asset = entity.phAsset; + if (!asset) { + [handler replyError:@"Not exists."]; + return; + } + + if (asset.isLivePhoto) { + PHContentEditingInputRequestOptions *options = [PHContentEditingInputRequestOptions new]; + options.networkAccessAllowed = YES; + [asset requestContentEditingInputWithOptions:options completionHandler:^(PHContentEditingInput * _Nullable contentEditingInput, NSDictionary * _Nonnull info) { + if (!contentEditingInput) { + [handler replyError:@"Failed to obtain the content request."]; + return; + } + PHLivePhotoEditingContext *context = [[PHLivePhotoEditingContext alloc] initWithLivePhotoEditingInput:contentEditingInput]; + if (!context) { + [handler replyError:@"Failed to obtain the Live Photo's context."]; + return; + } + NSTimeInterval time = CMTimeGetSeconds(context.duration); + [handler reply:@(time)]; + }]; + return; + } + + [handler reply:@(entity.duration)]; + return; +} + + - (NSString *)getTitleAsyncWithAssetId:(NSString *)assetId subtype:(int)subtype isOrigin:(BOOL)isOrigin fileType:(AVFileType)fileType { - PHAsset *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:nil].firstObject; + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]]; + PHAsset *asset = [self getFirstObjFromFetchResult:fetchResult]; if (asset) { return [asset filenameWithOptions:subtype isOrigin:isOrigin fileType:fileType]; } @@ -1293,7 +1335,7 @@ - (NSString *)getTitleAsyncWithAssetId:(NSString *)assetId } - (NSString *)getMimeTypeAsyncWithAssetId:(NSString *)assetId { - PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:nil]; + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]]; PHAsset *asset = [self getFirstObjFromFetchResult:fetchResult]; if (asset) { return [asset mimeType]; @@ -1304,7 +1346,7 @@ - (NSString *)getMimeTypeAsyncWithAssetId:(NSString *)assetId { - (void)getMediaUrl:(NSString *)assetId resultHandler:(NSObject *)handler progressHandler:(NSObject *)progressHandler { - PHAsset *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:nil].firstObject; + PHAsset *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:[self singleFetchOptions]].firstObject; if (@available(iOS 9.1, *)) { if ((asset.mediaSubtypes & PHAssetMediaSubtypePhotoLive) == PHAssetMediaSubtypePhotoLive) { @@ -1425,20 +1467,17 @@ - (void)copyAssetWithId:(NSString *)id return; } - __block PHFetchResult *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[id] options:nil]; + __block PHFetchResult *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[id] options:[self singleFetchOptions]]; NSError *error; - [PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{ PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:collection]; [request addAssets:asset]; } error:&error]; - if (error) { block(nil, error); - return; + } else { + block(assetEntity, nil); } - - block(assetEntity, nil); } - (void)createFolderWithName:(NSString *)name parentId:(NSString *)id block:(void (^)(NSString *newId, NSObject *error))block { @@ -1541,7 +1580,7 @@ - (void)createAlbumWithName:(NSString *)name parentId:(NSString *)id block:(void } } -- (void)removeInAlbumWithAssetId:(NSArray *)id albumId:(NSString *)albumId block:(void (^)(NSObject *error))block { +- (void)removeInAlbumWithAssetId:(NSArray *)ids albumId:(NSString *)albumId block:(void (^)(NSObject *error))block { PHFetchResult *result = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[albumId] options:nil]; PHAssetCollection *collection; if (result && result.count > 0) { @@ -1556,7 +1595,9 @@ - (void)removeInAlbumWithAssetId:(NSArray *)id albumId:(NSString *)albumId block return; } - PHFetchResult *assetResult = [PHAsset fetchAssetsWithLocalIdentifiers:id options:nil]; + PHFetchOptions *options = [PHFetchOptions new]; + options.fetchLimit = ids.count; + PHFetchResult *assetResult = [PHAsset fetchAssetsWithLocalIdentifiers:ids options:options]; NSError *error; [PHPhotoLibrary.sharedPhotoLibrary performChangesAndWait:^{ @@ -1626,7 +1667,7 @@ - (void)removeCollectionWithId:(NSString *)id type:(int)type block:(void (^)(NSO } - (void)favoriteWithId:(NSString *)id favorite:(BOOL)favorite block:(void (^)(BOOL result, NSObject *error))block { - PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[id] options:nil]; + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[id] options:[self singleFetchOptions]]; PHAsset *asset = [self getFirstObjFromFetchResult:fetchResult]; if (!asset) { block(NO, [NSString stringWithFormat:@"Asset %@ not found.", id]); @@ -1688,19 +1729,24 @@ - (void)clearFileCache { #pragma mark cache thumb -- (void)requestCacheAssetsThumb:(NSArray *)identifiers option:(PMThumbLoadOption *)option { - PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:identifiers options:nil]; +- (void)requestCacheAssetsThumb:(NSArray *)ids option:(PMThumbLoadOption *)option { + PHFetchOptions *fetchOptions = [PHFetchOptions new]; + fetchOptions.fetchLimit = ids.count; + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:ids options:fetchOptions]; NSMutableArray *array = [NSMutableArray new]; for (id asset in fetchResult) { [array addObject:asset]; } - PHImageRequestOptions *options = [PHImageRequestOptions new]; - options.resizeMode = options.resizeMode; - options.deliveryMode = option.deliveryMode; + PHImageRequestOptions *requestOptions = [PHImageRequestOptions new]; + requestOptions.resizeMode = option.resizeMode; + requestOptions.deliveryMode = option.deliveryMode; - [self.cachingManager startCachingImagesForAssets:array targetSize:[option makeSize] contentMode:option.contentMode options:options]; + [self.cachingManager startCachingImagesForAssets:array + targetSize:[option makeSize] + contentMode:option.contentMode + options:requestOptions]; } - (void)cancelCacheRequests { diff --git a/lib/src/internal/constants.dart b/lib/src/internal/constants.dart index b5cbba4a..87ca2f00 100644 --- a/lib/src/internal/constants.dart +++ b/lib/src/internal/constants.dart @@ -54,6 +54,7 @@ class PMConstants { static const String mCancelCacheRequests = 'cancelCacheRequests'; static const String mRequestCacheAssetsThumb = 'requestCacheAssetsThumb'; static const String mIsLocallyAvailable = 'isLocallyAvailable'; + static const String mGetDurationWithOptions = 'getDurationWithOptions'; static const String mCreateAlbum = 'createAlbum'; static const String mCreateFolder = 'createFolder'; static const String mRemoveInAlbum = 'removeInAlbum'; diff --git a/lib/src/internal/plugin.dart b/lib/src/internal/plugin.dart index 0048c2e0..76499740 100644 --- a/lib/src/internal/plugin.dart +++ b/lib/src/internal/plugin.dart @@ -632,6 +632,23 @@ class PhotoManagerPlugin with BasePlugin, IosPlugin, AndroidPlugin, OhosPlugin { return ConvertUtils.convertToAssetList(result.cast()); } + Future getDurationWithOptions(String id, {int? subtype}) async { + if (Platform.isIOS || Platform.isMacOS) { + if (subtype != null) { + final result = await _channel.invokeMethod( + PMConstants.mGetDurationWithOptions, + { + 'id': id, + 'subtype': subtype, + }, + ); + return result as int; + } + } + final entity = await AssetEntity.fromId(id); + return entity!.duration; + } + Future isLocallyAvailable( String id, { bool isOrigin = false, diff --git a/lib/src/types/entity.dart b/lib/src/types/entity.dart index f9741816..bc019b46 100644 --- a/lib/src/types/entity.dart +++ b/lib/src/types/entity.dart @@ -435,6 +435,16 @@ class AssetEntity { /// * [videoDuration] which is a duration getter for videos. final int duration; + /// Obtain the duration with the given options. + /// + /// [withSubtype] only works on iOS/macOS. + Future durationWithOptions({bool withSubtype = false}) async { + if (withSubtype) { + return plugin.getDurationWithOptions(id, subtype: subtype); + } + return duration; + } + /// The width of the asset. /// /// This field could be 0 in cases that EXIF info is failed to parse.