2015-09-08 15:58:13 +00:00
|
|
|
/**
|
2018-09-11 22:27:47 +00:00
|
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
2015-09-08 15:58:13 +00:00
|
|
|
*
|
2018-02-17 02:24:55 +00:00
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
2015-09-08 15:58:13 +00:00
|
|
|
*/
|
|
|
|
|
2019-02-24 04:29:24 +00:00
|
|
|
#import "RNCCameraRollManager.h"
|
2015-09-08 15:58:13 +00:00
|
|
|
|
|
|
|
#import <CoreLocation/CoreLocation.h>
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#import <UIKit/UIKit.h>
|
2017-11-29 20:07:49 +00:00
|
|
|
#import <Photos/Photos.h>
|
2018-06-19 20:18:51 +00:00
|
|
|
#import <dlfcn.h>
|
|
|
|
#import <objc/runtime.h>
|
2019-01-08 00:11:52 +00:00
|
|
|
#import <MobileCoreServices/UTType.h>
|
2015-09-08 15:58:13 +00:00
|
|
|
|
2016-11-23 15:47:52 +00:00
|
|
|
#import <React/RCTBridge.h>
|
|
|
|
#import <React/RCTConvert.h>
|
|
|
|
#import <React/RCTLog.h>
|
|
|
|
#import <React/RCTUtils.h>
|
|
|
|
|
2019-02-24 05:48:37 +00:00
|
|
|
#import "RNCAssetsLibraryRequestHandler.h"
|
2015-09-08 15:58:13 +00:00
|
|
|
|
2019-01-08 00:11:52 +00:00
|
|
|
@implementation RCTConvert (PHAssetCollectionSubtype)
|
|
|
|
|
|
|
|
RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{
|
|
|
|
@"album": @(PHAssetCollectionSubtypeAny),
|
2019-03-02 07:54:39 +00:00
|
|
|
@"all": @(PHAssetCollectionSubtypeSmartAlbumUserLibrary),
|
2019-01-08 00:11:52 +00:00
|
|
|
@"event": @(PHAssetCollectionSubtypeAlbumSyncedEvent),
|
|
|
|
@"faces": @(PHAssetCollectionSubtypeAlbumSyncedFaces),
|
|
|
|
@"library": @(PHAssetCollectionSubtypeSmartAlbumUserLibrary),
|
|
|
|
@"photo-stream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream), // incorrect, but legacy
|
|
|
|
@"photostream": @(PHAssetCollectionSubtypeAlbumMyPhotoStream),
|
2019-08-14 19:00:20 +00:00
|
|
|
@"saved-photos": @(PHAssetCollectionSubtypeAny), // incorrect, but legacy correspondence in PHAssetCollectionSubtype
|
2019-01-08 00:11:52 +00:00
|
|
|
@"savedphotos": @(PHAssetCollectionSubtypeAny), // This was ALAssetsGroupSavedPhotos, seems to have no direct correspondence in PHAssetCollectionSubtype
|
|
|
|
}), PHAssetCollectionSubtypeAny, integerValue)
|
|
|
|
|
2018-12-22 08:16:33 +00:00
|
|
|
|
2019-01-08 00:11:52 +00:00
|
|
|
@end
|
|
|
|
|
|
|
|
@implementation RCTConvert (PHFetchOptions)
|
|
|
|
|
|
|
|
+ (PHFetchOptions *)PHFetchOptionsFromMediaType:(NSString *)mediaType
|
2020-02-05 09:20:54 +00:00
|
|
|
fromTime:(NSUInteger)fromTime
|
|
|
|
toTime:(NSUInteger)toTime
|
2018-12-22 08:16:33 +00:00
|
|
|
{
|
2019-01-08 00:11:52 +00:00
|
|
|
// This is not exhaustive in terms of supported media type predicates; more can be added in the future
|
|
|
|
NSString *const lowercase = [mediaType lowercaseString];
|
2020-02-05 09:20:54 +00:00
|
|
|
NSMutableArray *format = [NSMutableArray new];
|
|
|
|
NSMutableArray *arguments = [NSMutableArray new];
|
2019-08-14 19:00:20 +00:00
|
|
|
|
2019-01-08 00:11:52 +00:00
|
|
|
if ([lowercase isEqualToString:@"photos"]) {
|
2020-02-05 09:20:54 +00:00
|
|
|
[format addObject:@"mediaType = %d"];
|
|
|
|
[arguments addObject:@(PHAssetMediaTypeImage)];
|
2019-01-08 00:11:52 +00:00
|
|
|
} else if ([lowercase isEqualToString:@"videos"]) {
|
2020-02-05 09:20:54 +00:00
|
|
|
[format addObject:@"mediaType = %d"];
|
|
|
|
[arguments addObject:@(PHAssetMediaTypeVideo)];
|
2019-01-08 00:11:52 +00:00
|
|
|
} else {
|
|
|
|
if (![lowercase isEqualToString:@"all"]) {
|
|
|
|
RCTLogError(@"Invalid filter option: '%@'. Expected one of 'photos',"
|
|
|
|
"'videos' or 'all'.", mediaType);
|
|
|
|
}
|
2015-11-03 22:45:46 +00:00
|
|
|
}
|
2020-02-05 09:20:54 +00:00
|
|
|
|
|
|
|
if (fromTime > 0) {
|
|
|
|
NSDate* fromDate = [NSDate dateWithTimeIntervalSince1970:fromTime/1000];
|
|
|
|
[format addObject:@"creationDate > %@"];
|
|
|
|
[arguments addObject:fromDate];
|
|
|
|
}
|
|
|
|
if (toTime > 0) {
|
|
|
|
NSDate* toDate = [NSDate dateWithTimeIntervalSince1970:toTime/1000];
|
2020-06-16 10:20:51 +00:00
|
|
|
[format addObject:@"creationDate <= %@"];
|
2020-02-05 09:20:54 +00:00
|
|
|
[arguments addObject:toDate];
|
|
|
|
}
|
|
|
|
|
|
|
|
// This case includes the "all" mediatype
|
|
|
|
PHFetchOptions *const options = [PHFetchOptions new];
|
|
|
|
if ([format count] > 0) {
|
|
|
|
options.predicate = [NSPredicate predicateWithFormat:[format componentsJoinedByString:@" AND "] argumentArray:arguments];
|
|
|
|
}
|
|
|
|
return options;
|
2015-11-03 22:45:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
2019-02-24 04:29:24 +00:00
|
|
|
@implementation RNCCameraRollManager
|
2015-09-08 15:58:13 +00:00
|
|
|
|
2019-03-03 06:39:57 +00:00
|
|
|
RCT_EXPORT_MODULE(RNCCameraRoll)
|
2015-09-08 15:58:13 +00:00
|
|
|
|
|
|
|
@synthesize bridge = _bridge;
|
|
|
|
|
2018-12-22 08:16:33 +00:00
|
|
|
static NSString *const kErrorUnableToSave = @"E_UNABLE_TO_SAVE";
|
2019-01-08 00:11:52 +00:00
|
|
|
static NSString *const kErrorUnableToLoad = @"E_UNABLE_TO_LOAD";
|
2016-01-21 16:07:01 +00:00
|
|
|
|
2019-01-10 21:07:15 +00:00
|
|
|
static NSString *const kErrorAuthRestricted = @"E_PHOTO_LIBRARY_AUTH_RESTRICTED";
|
|
|
|
static NSString *const kErrorAuthDenied = @"E_PHOTO_LIBRARY_AUTH_DENIED";
|
|
|
|
|
|
|
|
typedef void (^PhotosAuthorizedBlock)(void);
|
|
|
|
|
|
|
|
static void requestPhotoLibraryAccess(RCTPromiseRejectBlock reject, PhotosAuthorizedBlock authorizedBlock) {
|
|
|
|
PHAuthorizationStatus authStatus = [PHPhotoLibrary authorizationStatus];
|
|
|
|
if (authStatus == PHAuthorizationStatusRestricted) {
|
|
|
|
reject(kErrorAuthRestricted, @"Access to photo library is restricted", nil);
|
|
|
|
} else if (authStatus == PHAuthorizationStatusAuthorized) {
|
|
|
|
authorizedBlock();
|
|
|
|
} else if (authStatus == PHAuthorizationStatusNotDetermined) {
|
|
|
|
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
|
|
|
|
requestPhotoLibraryAccess(reject, authorizedBlock);
|
|
|
|
}];
|
|
|
|
} else {
|
|
|
|
reject(kErrorAuthDenied, @"Access to photo library was denied", nil);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Allow CameraRoll to export videos
Summary:
This PR adds the ability to export videos to the CameraRoll on both Android and iOS (previously only photos were possible, at least on iOS). The API has changed as follows:
```
// old
saveImageWithTag(tag: string): Promise<string>
// new
saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<string>
```
if no `type` parameter is passed, `video` is inferred if the tag ends with ".mov" or ".mp4", otherwise `photo` is assumed.
I've left in the `saveImageWithTag` method for now with a deprecation warning.
**Test plan (required)**
I created the following very simple app to test exporting photos and videos to the CameraRoll, and ran it on both iOS and Android. The functionality works as intended on both platforms.
```js
// index.js
/**
* Sample React Native App
* https://github.com/facebook/react-native
* flow
*/
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
CameraRoll,
} from 'react-native';
import FS fro
Closes https://github.com/facebook/react-native/pull/7988
Differential Revision: D3401251
Pulled By: nicklockwood
fbshipit-source-id: af3fc24e6fa5b84ac377e9173f3709c6f9795f20
2016-06-07 23:37:48 +00:00
|
|
|
RCT_EXPORT_METHOD(saveToCameraRoll:(NSURLRequest *)request
|
2019-08-14 19:00:20 +00:00
|
|
|
options:(NSDictionary *)options
|
2016-01-21 16:07:01 +00:00
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject)
|
2015-09-08 15:58:13 +00:00
|
|
|
{
|
2019-01-08 00:11:52 +00:00
|
|
|
// We load images and videos differently.
|
|
|
|
// Images have many custom loaders which can load images from ALAssetsLibrary URLs, PHPhotoLibrary
|
|
|
|
// URLs, `data:` URIs, etc. Video URLs are passed directly through for now; it may be nice to support
|
|
|
|
// more ways of loading videos in the future.
|
|
|
|
__block NSURL *inputURI = nil;
|
2019-08-14 19:00:20 +00:00
|
|
|
__block PHFetchResult *photosAsset;
|
|
|
|
__block PHAssetCollection *collection;
|
|
|
|
__block PHObjectPlaceholder *placeholder;
|
2019-01-08 00:11:52 +00:00
|
|
|
|
|
|
|
void (^saveBlock)(void) = ^void() {
|
|
|
|
// performChanges and the completionHandler are called on
|
|
|
|
// arbitrary threads, not the main thread - this is safe
|
|
|
|
// for now since all JS is queued and executed on a single thread.
|
|
|
|
// We should reevaluate this if that assumption changes.
|
|
|
|
|
2019-08-14 19:00:20 +00:00
|
|
|
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
|
|
|
PHAssetChangeRequest *assetRequest ;
|
|
|
|
if ([options[@"type"] isEqualToString:@"video"]) {
|
|
|
|
assetRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:inputURI];
|
2019-01-08 00:11:52 +00:00
|
|
|
} else {
|
2020-05-08 19:41:46 +00:00
|
|
|
NSData *data = [NSData dataWithContentsOfURL:inputURI];
|
|
|
|
UIImage *image = [UIImage imageWithData:data];
|
|
|
|
assetRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:image];
|
2019-01-08 00:11:52 +00:00
|
|
|
}
|
2019-08-14 19:00:20 +00:00
|
|
|
placeholder = [assetRequest placeholderForCreatedAsset];
|
|
|
|
if (![options[@"album"] isEqualToString:@""]) {
|
|
|
|
photosAsset = [PHAsset fetchAssetsInAssetCollection:collection options:nil];
|
|
|
|
PHAssetCollectionChangeRequest *albumChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:collection assets:photosAsset];
|
|
|
|
[albumChangeRequest addAssets:@[placeholder]];
|
|
|
|
}
|
|
|
|
} completionHandler:^(BOOL success, NSError *error) {
|
2019-01-08 00:11:52 +00:00
|
|
|
if (success) {
|
|
|
|
NSString *uri = [NSString stringWithFormat:@"ph://%@", [placeholder localIdentifier]];
|
|
|
|
resolve(uri);
|
|
|
|
} else {
|
|
|
|
reject(kErrorUnableToSave, nil, error);
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
};
|
2019-08-14 19:00:20 +00:00
|
|
|
void (^saveWithOptions)(void) = ^void() {
|
|
|
|
if (![options[@"album"] isEqualToString:@""]) {
|
|
|
|
|
|
|
|
PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init];
|
|
|
|
fetchOptions.predicate = [NSPredicate predicateWithFormat:@"title = %@", options[@"album"] ];
|
|
|
|
collection = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum
|
|
|
|
subtype:PHAssetCollectionSubtypeAny
|
|
|
|
options:fetchOptions].firstObject;
|
|
|
|
// Create the album
|
|
|
|
if (!collection) {
|
|
|
|
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
|
|
|
PHAssetCollectionChangeRequest *createAlbum = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:options[@"album"]];
|
|
|
|
placeholder = [createAlbum placeholderForCreatedAssetCollection];
|
|
|
|
} completionHandler:^(BOOL success, NSError *error) {
|
|
|
|
if (success) {
|
|
|
|
PHFetchResult *collectionFetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[placeholder.localIdentifier]
|
|
|
|
options:nil];
|
|
|
|
collection = collectionFetchResult.firstObject;
|
|
|
|
saveBlock();
|
|
|
|
} else {
|
|
|
|
reject(kErrorUnableToSave, nil, error);
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
} else {
|
|
|
|
saveBlock();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
saveBlock();
|
|
|
|
}
|
|
|
|
};
|
2019-03-03 06:39:57 +00:00
|
|
|
|
2019-01-10 21:07:15 +00:00
|
|
|
void (^loadBlock)(void) = ^void() {
|
2020-02-08 02:17:48 +00:00
|
|
|
inputURI = request.URL;
|
|
|
|
saveWithOptions();
|
2019-01-10 21:07:15 +00:00
|
|
|
};
|
2019-03-03 06:39:57 +00:00
|
|
|
|
2019-01-10 21:07:15 +00:00
|
|
|
requestPhotoLibraryAccess(reject, loadBlock);
|
2015-09-08 15:58:13 +00:00
|
|
|
}
|
2020-02-08 03:17:56 +00:00
|
|
|
|
|
|
|
RCT_EXPORT_METHOD(getAlbums:(NSDictionary *)params
|
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject)
|
|
|
|
{
|
|
|
|
NSString *const mediaType = [params objectForKey:@"assetType"] ? [RCTConvert NSString:params[@"assetType"]] : @"All";
|
|
|
|
PHFetchOptions* options = [[PHFetchOptions alloc] init];
|
|
|
|
PHFetchResult<PHAssetCollection *> *const assetCollectionFetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny options:options];
|
|
|
|
NSMutableArray * result = [NSMutableArray new];
|
|
|
|
[assetCollectionFetchResult enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
|
|
PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType fromTime:0 toTime:0];
|
|
|
|
// Enumerate assets within the collection
|
|
|
|
PHFetchResult<PHAsset *> *const assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:obj options:assetFetchOptions];
|
|
|
|
if (assetsFetchResult.count > 0) {
|
|
|
|
[result addObject:@{
|
|
|
|
@"title": [obj localizedTitle],
|
|
|
|
@"count": @(assetsFetchResult.count)
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
resolve(result);
|
|
|
|
}
|
2015-09-08 15:58:13 +00:00
|
|
|
|
2016-01-21 16:07:01 +00:00
|
|
|
static void RCTResolvePromise(RCTPromiseResolveBlock resolve,
|
|
|
|
NSArray<NSDictionary<NSString *, id> *> *assets,
|
|
|
|
BOOL hasNextPage)
|
2015-09-08 15:58:13 +00:00
|
|
|
{
|
|
|
|
if (!assets.count) {
|
2016-02-10 15:24:38 +00:00
|
|
|
resolve(@{
|
2015-11-14 18:25:00 +00:00
|
|
|
@"edges": assets,
|
|
|
|
@"page_info": @{
|
|
|
|
@"has_next_page": @NO,
|
|
|
|
}
|
2016-02-10 15:24:38 +00:00
|
|
|
});
|
2015-09-08 15:58:13 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-02-10 15:24:38 +00:00
|
|
|
resolve(@{
|
2015-11-14 18:25:00 +00:00
|
|
|
@"edges": assets,
|
|
|
|
@"page_info": @{
|
|
|
|
@"start_cursor": assets[0][@"node"][@"image"][@"uri"],
|
|
|
|
@"end_cursor": assets[assets.count - 1][@"node"][@"image"][@"uri"],
|
|
|
|
@"has_next_page": @(hasNextPage),
|
|
|
|
}
|
2016-02-10 15:24:38 +00:00
|
|
|
});
|
2015-09-08 15:58:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
2016-01-21 16:07:01 +00:00
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject)
|
2015-09-08 15:58:13 +00:00
|
|
|
{
|
2016-08-18 14:16:26 +00:00
|
|
|
checkPhotoLibraryConfig();
|
|
|
|
|
2019-01-08 00:11:52 +00:00
|
|
|
NSUInteger const first = [RCTConvert NSInteger:params[@"first"]];
|
|
|
|
NSString *const afterCursor = [RCTConvert NSString:params[@"after"]];
|
|
|
|
NSString *const groupName = [RCTConvert NSString:params[@"groupName"]];
|
|
|
|
NSString *const groupTypes = [[RCTConvert NSString:params[@"groupTypes"]] lowercaseString];
|
|
|
|
NSString *const mediaType = [RCTConvert NSString:params[@"assetType"]];
|
2020-02-05 09:20:54 +00:00
|
|
|
NSUInteger const fromTime = [RCTConvert NSInteger:params[@"fromTime"]];
|
|
|
|
NSUInteger const toTime = [RCTConvert NSInteger:params[@"toTime"]];
|
2019-01-08 00:11:52 +00:00
|
|
|
NSArray<NSString *> *const mimeTypes = [RCTConvert NSStringArray:params[@"mimeTypes"]];
|
2020-06-16 10:20:51 +00:00
|
|
|
NSArray<NSString *> *const include = [RCTConvert NSStringArray:params[@"include"]];
|
|
|
|
|
|
|
|
BOOL __block includeFilename = [include indexOfObject:@"filename"] != NSNotFound;
|
|
|
|
BOOL __block includeFileSize = [include indexOfObject:@"fileSize"] != NSNotFound;
|
|
|
|
BOOL __block includeLocation = [include indexOfObject:@"location"] != NSNotFound;
|
2020-06-23 05:45:20 +00:00
|
|
|
BOOL __block includeImageSize = [include indexOfObject:@"imageSize"] != NSNotFound;
|
|
|
|
BOOL __block includePlayableDuration = [include indexOfObject:@"playableDuration"] != NSNotFound;
|
2019-08-14 19:00:20 +00:00
|
|
|
|
2019-01-08 00:11:52 +00:00
|
|
|
// If groupTypes is "all", we want to fetch the SmartAlbum "all photos". Otherwise, all
|
|
|
|
// other groupTypes values require the "album" collection type.
|
|
|
|
PHAssetCollectionType const collectionType = ([groupTypes isEqualToString:@"all"]
|
|
|
|
? PHAssetCollectionTypeSmartAlbum
|
|
|
|
: PHAssetCollectionTypeAlbum);
|
|
|
|
PHAssetCollectionSubtype const collectionSubtype = [RCTConvert PHAssetCollectionSubtype:groupTypes];
|
2019-08-14 19:00:20 +00:00
|
|
|
|
2019-01-08 00:11:52 +00:00
|
|
|
// Predicate for fetching assets within a collection
|
2020-02-05 09:20:54 +00:00
|
|
|
PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType fromTime:fromTime toTime:toTime];
|
2020-06-16 10:20:51 +00:00
|
|
|
// We can directly set the limit if we guarantee every image fetched will be
|
|
|
|
// added to the output array within the `collectAsset` block
|
|
|
|
BOOL collectAssetMayOmitAsset = !!afterCursor || [mimeTypes count] > 0;
|
|
|
|
if (!collectAssetMayOmitAsset) {
|
|
|
|
// We set the fetchLimit to first + 1 so that `hasNextPage` will be set
|
|
|
|
// correctly:
|
|
|
|
// - If the user set `first: 10` and there are 11 photos, `hasNextPage`
|
|
|
|
// will be set to true below inside of `collectAsset`
|
|
|
|
// - If the user set `first: 10` and there are 10 photos, `hasNextPage`
|
|
|
|
// will not be set, as expected
|
|
|
|
assetFetchOptions.fetchLimit = first + 1;
|
|
|
|
}
|
2019-01-08 00:11:52 +00:00
|
|
|
assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
|
2019-08-14 19:00:20 +00:00
|
|
|
|
2015-09-08 15:58:13 +00:00
|
|
|
BOOL __block foundAfter = NO;
|
|
|
|
BOOL __block hasNextPage = NO;
|
2016-01-21 16:07:01 +00:00
|
|
|
BOOL __block resolvedPromise = NO;
|
2015-11-14 18:25:00 +00:00
|
|
|
NSMutableArray<NSDictionary<NSString *, id> *> *assets = [NSMutableArray new];
|
2019-08-14 19:00:20 +00:00
|
|
|
|
2019-01-08 00:11:52 +00:00
|
|
|
// Filter collection name ("group")
|
|
|
|
PHFetchOptions *const collectionFetchOptions = [PHFetchOptions new];
|
|
|
|
collectionFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate" ascending:NO]];
|
|
|
|
if (groupName != nil) {
|
2020-03-26 10:01:09 +00:00
|
|
|
collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", groupName];
|
2019-01-08 00:11:52 +00:00
|
|
|
}
|
2019-05-07 19:44:02 +00:00
|
|
|
|
|
|
|
BOOL __block stopCollections_;
|
|
|
|
NSString __block *currentCollectionName;
|
2019-03-03 06:39:57 +00:00
|
|
|
|
2019-01-10 21:07:15 +00:00
|
|
|
requestPhotoLibraryAccess(reject, ^{
|
2019-05-07 19:44:02 +00:00
|
|
|
void (^collectAsset)(PHAsset*, NSUInteger, BOOL*) = ^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) {
|
|
|
|
NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]];
|
2020-06-16 10:20:51 +00:00
|
|
|
NSString *_Nullable originalFilename = NULL;
|
|
|
|
PHAssetResource *_Nullable resource = NULL;
|
|
|
|
NSNumber* fileSize = [NSNumber numberWithInt:0];
|
|
|
|
|
|
|
|
if (includeFilename || includeFileSize || [mimeTypes count] > 0) {
|
|
|
|
// Get underlying resources of an asset - this includes files as well as details about edited PHAssets
|
|
|
|
// This is required for the filename and mimeType filtering
|
|
|
|
NSArray<PHAssetResource *> *const assetResources = [PHAssetResource assetResourcesForAsset:asset];
|
|
|
|
resource = [assetResources firstObject];
|
|
|
|
originalFilename = resource.originalFilename;
|
|
|
|
fileSize = [resource valueForKey:@"fileSize"];
|
2020-06-16 10:05:12 +00:00
|
|
|
}
|
2020-06-16 10:20:51 +00:00
|
|
|
|
|
|
|
// WARNING: If you add any code to `collectAsset` that may skip adding an
|
|
|
|
// asset to the `assets` output array, you should do it inside this
|
|
|
|
// block and ensure the logic for `collectAssetMayOmitAsset` above is
|
|
|
|
// updated
|
|
|
|
if (collectAssetMayOmitAsset) {
|
|
|
|
if (afterCursor && !foundAfter) {
|
|
|
|
if ([afterCursor isEqualToString:uri]) {
|
|
|
|
foundAfter = YES;
|
|
|
|
}
|
|
|
|
return; // skip until we get to the first one
|
|
|
|
}
|
2019-03-03 06:39:57 +00:00
|
|
|
|
|
|
|
|
2020-06-16 10:20:51 +00:00
|
|
|
if ([mimeTypes count] > 0 && resource) {
|
|
|
|
CFStringRef const uti = (__bridge CFStringRef _Nonnull)(resource.uniformTypeIdentifier);
|
|
|
|
NSString *const mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
|
2019-03-03 06:39:57 +00:00
|
|
|
|
2020-06-16 10:20:51 +00:00
|
|
|
BOOL __block mimeTypeFound = NO;
|
|
|
|
[mimeTypes enumerateObjectsUsingBlock:^(NSString * _Nonnull mimeTypeFilter, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
|
|
if ([mimeType isEqualToString:mimeTypeFilter]) {
|
|
|
|
mimeTypeFound = YES;
|
|
|
|
*stop = YES;
|
|
|
|
}
|
|
|
|
}];
|
2020-06-16 10:05:12 +00:00
|
|
|
|
2020-06-16 10:20:51 +00:00
|
|
|
if (!mimeTypeFound) {
|
|
|
|
return;
|
|
|
|
}
|
2019-01-08 00:11:52 +00:00
|
|
|
}
|
2019-05-07 19:44:02 +00:00
|
|
|
}
|
2019-03-03 06:39:57 +00:00
|
|
|
|
2019-05-07 19:44:02 +00:00
|
|
|
// If we've accumulated enough results to resolve a single promise
|
|
|
|
if (first == assets.count) {
|
|
|
|
*stopAssets = YES;
|
|
|
|
stopCollections_ = YES;
|
|
|
|
hasNextPage = YES;
|
|
|
|
RCTAssert(resolvedPromise == NO, @"Resolved the promise before we finished processing the results.");
|
|
|
|
RCTResolvePromise(resolve, assets, hasNextPage);
|
|
|
|
resolvedPromise = YES;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
NSString *const assetMediaTypeLabel = (asset.mediaType == PHAssetMediaTypeVideo
|
|
|
|
? @"video"
|
|
|
|
: (asset.mediaType == PHAssetMediaTypeImage
|
|
|
|
? @"image"
|
|
|
|
: (asset.mediaType == PHAssetMediaTypeAudio
|
|
|
|
? @"audio"
|
|
|
|
: @"unknown")));
|
|
|
|
CLLocation *const loc = asset.location;
|
|
|
|
|
|
|
|
[assets addObject:@{
|
|
|
|
@"node": @{
|
|
|
|
@"type": assetMediaTypeLabel, // TODO: switch to mimeType?
|
|
|
|
@"group_name": currentCollectionName,
|
|
|
|
@"image": @{
|
|
|
|
@"uri": uri,
|
2020-06-16 10:20:51 +00:00
|
|
|
@"filename": (includeFilename && originalFilename ? originalFilename : [NSNull null]),
|
2020-06-23 05:45:20 +00:00
|
|
|
@"height": (includeImageSize ? @([asset pixelHeight]) : [NSNull null]),
|
|
|
|
@"width": (includeImageSize ? @([asset pixelWidth]) : [NSNull null]),
|
2020-06-16 10:20:51 +00:00
|
|
|
@"fileSize": (includeFileSize ? fileSize : [NSNull null]),
|
2020-06-23 05:45:20 +00:00
|
|
|
@"playableDuration": (includePlayableDuration && asset.mediaType != PHAssetMediaTypeImage
|
|
|
|
? @([asset duration]) // fractional seconds
|
|
|
|
: [NSNull null])
|
2019-05-07 19:44:02 +00:00
|
|
|
},
|
|
|
|
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
|
2020-06-16 10:20:51 +00:00
|
|
|
@"location": (includeLocation && loc ? @{
|
2019-05-07 19:44:02 +00:00
|
|
|
@"latitude": @(loc.coordinate.latitude),
|
|
|
|
@"longitude": @(loc.coordinate.longitude),
|
|
|
|
@"altitude": @(loc.altitude),
|
|
|
|
@"heading": @(loc.course),
|
|
|
|
@"speed": @(loc.speed), // speed in m/s
|
2020-06-16 10:20:51 +00:00
|
|
|
} : [NSNull null])
|
2019-05-07 19:44:02 +00:00
|
|
|
}
|
2019-01-08 00:11:52 +00:00
|
|
|
}];
|
2019-05-07 19:44:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if ([groupTypes isEqualToString:@"all"]) {
|
|
|
|
PHFetchResult <PHAsset *> *const assetFetchResult = [PHAsset fetchAssetsWithOptions: assetFetchOptions];
|
|
|
|
currentCollectionName = @"All Photos";
|
|
|
|
[assetFetchResult enumerateObjectsUsingBlock:collectAsset];
|
|
|
|
} else {
|
|
|
|
PHFetchResult<PHAssetCollection *> *const assetCollectionFetchResult = [PHAssetCollection fetchAssetCollectionsWithType:collectionType subtype:collectionSubtype options:collectionFetchOptions];
|
|
|
|
[assetCollectionFetchResult enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull assetCollection, NSUInteger collectionIdx, BOOL * _Nonnull stopCollections) {
|
|
|
|
// Enumerate assets within the collection
|
|
|
|
PHFetchResult<PHAsset *> *const assetsFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:assetFetchOptions];
|
|
|
|
currentCollectionName = [assetCollection localizedTitle];
|
|
|
|
[assetsFetchResult enumerateObjectsUsingBlock:collectAsset];
|
|
|
|
*stopCollections = stopCollections_;
|
|
|
|
}];
|
|
|
|
}
|
2019-03-03 06:39:57 +00:00
|
|
|
|
2019-01-10 21:07:15 +00:00
|
|
|
// If we get this far and haven't resolved the promise yet, we reached the end of the list of photos
|
|
|
|
if (!resolvedPromise) {
|
|
|
|
hasNextPage = NO;
|
|
|
|
RCTResolvePromise(resolve, assets, hasNextPage);
|
|
|
|
resolvedPromise = YES;
|
|
|
|
}
|
|
|
|
});
|
2015-09-08 15:58:13 +00:00
|
|
|
}
|
|
|
|
|
2017-11-29 20:07:49 +00:00
|
|
|
RCT_EXPORT_METHOD(deletePhotos:(NSArray<NSString *>*)assets
|
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject)
|
|
|
|
{
|
2019-11-07 13:13:24 +00:00
|
|
|
NSMutableArray *convertedAssets = [NSMutableArray array];
|
|
|
|
|
|
|
|
for (NSString *asset in assets) {
|
|
|
|
[convertedAssets addObject: [asset stringByReplacingOccurrencesOfString:@"ph://" withString:@""]];
|
2019-08-14 19:00:20 +00:00
|
|
|
}
|
2019-11-07 13:13:24 +00:00
|
|
|
|
|
|
|
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
|
|
|
PHFetchResult<PHAsset *> *fetched =
|
|
|
|
[PHAsset fetchAssetsWithLocalIdentifiers:convertedAssets options:nil];
|
|
|
|
[PHAssetChangeRequest deleteAssets:fetched];
|
|
|
|
}
|
2017-11-29 20:07:49 +00:00
|
|
|
completionHandler:^(BOOL success, NSError *error) {
|
2019-08-14 19:00:20 +00:00
|
|
|
if (success == YES) {
|
|
|
|
resolve(@(success));
|
2017-11-29 20:07:49 +00:00
|
|
|
}
|
2019-08-14 19:00:20 +00:00
|
|
|
else {
|
|
|
|
reject(@"Couldn't delete", @"Couldn't delete assets", error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
];
|
2017-11-29 20:07:49 +00:00
|
|
|
}
|
2023-10-14 14:59:36 +00:00
|
|
|
RCT_EXPORT_METHOD(getPhotosCountiOS:(NSString *)blank
|
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject)
|
|
|
|
{
|
|
|
|
__block NSInteger intTotalCount=0;
|
|
|
|
PHFetchOptions *allPhotosOptions = [PHFetchOptions new];
|
|
|
|
allPhotosOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType == %d ",PHAssetMediaTypeImage];
|
|
|
|
PHFetchResult *allPhotosResult = [PHAsset fetchAssetsWithOptions:allPhotosOptions];
|
|
|
|
intTotalCount+=allPhotosResult.count;
|
|
|
|
|
|
|
|
resolve(@(intTotalCount));
|
|
|
|
}
|
|
|
|
|
|
|
|
RCT_EXPORT_METHOD(getFavoritesiOS:(NSString *)blank
|
|
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
|
|
reject:(RCTPromiseRejectBlock)reject)
|
|
|
|
{
|
|
|
|
__block NSInteger intTotalCount=0;
|
|
|
|
PHFetchOptions *fetchOptions = [PHFetchOptions new];
|
|
|
|
NSString *format = @"(favorite == true)";
|
|
|
|
fetchOptions.predicate = [NSPredicate predicateWithFormat:format];
|
|
|
|
PHFetchResult<PHAsset *> *const assetsFetchResult = [PHAsset fetchAssetsWithOptions:fetchOptions];
|
|
|
|
PHAsset *imageAsset = [assetsFetchResult firstObject];
|
|
|
|
NSMutableArray * result = [NSMutableArray new];
|
|
|
|
|
|
|
|
|
|
|
|
for (PHAsset* asset in assetsFetchResult) {
|
|
|
|
NSArray *resources = [PHAssetResource assetResourcesForAsset:asset ];
|
|
|
|
if ([resources count] < 1) continue;
|
|
|
|
NSString *orgFilename = ((PHAssetResource*)resources[0]).originalFilename;
|
|
|
|
NSString *uit = ((PHAssetResource*)resources[0]).uniformTypeIdentifier;
|
|
|
|
NSString *mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass((__bridge CFStringRef _Nonnull)(uit), kUTTagClassMIMEType));
|
|
|
|
CFStringRef extension = UTTypeCopyPreferredTagWithClass((__bridge CFStringRef _Nonnull)(uit), kUTTagClassFilenameExtension);
|
|
|
|
[result addObject:@{
|
|
|
|
@"width": @([asset pixelWidth]),
|
|
|
|
@"height": @([asset pixelHeight]),
|
|
|
|
@"filename": orgFilename ?: @"",
|
|
|
|
@"mimeType": mimeType ?: @"",
|
|
|
|
@"id": [asset localIdentifier],
|
|
|
|
@"creationDate": [asset creationDate],
|
|
|
|
@"uri": [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]],
|
|
|
|
@"duration": @([asset duration])
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[result addObject:@{
|
|
|
|
@"count": @(assetsFetchResult.count)
|
|
|
|
}];
|
|
|
|
resolve(result);
|
|
|
|
}
|
2017-11-29 20:07:49 +00:00
|
|
|
|
2016-08-18 14:16:26 +00:00
|
|
|
static void checkPhotoLibraryConfig()
|
|
|
|
{
|
|
|
|
#if RCT_DEV
|
|
|
|
if (![[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSPhotoLibraryUsageDescription"]) {
|
|
|
|
RCTLogError(@"NSPhotoLibraryUsageDescription key must be present in Info.plist to use camera roll.");
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2015-09-08 15:58:13 +00:00
|
|
|
@end
|