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];
|
|
|
|
[format addObject:@"creationDate < %@"];
|
|
|
|
[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;
|
|
|
|
__block UIImage *inputImage = 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-02-08 02:17:48 +00:00
|
|
|
assetRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:inputURI];
|
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"]];
|
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];
|
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) {
|
|
|
|
collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:@"localizedTitle == '%@'", groupName]];
|
|
|
|
}
|
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]];
|
|
|
|
if (afterCursor && !foundAfter) {
|
|
|
|
if ([afterCursor isEqualToString:uri]) {
|
|
|
|
foundAfter = YES;
|
2019-01-08 00:11:52 +00:00
|
|
|
}
|
2019-05-07 19:44:02 +00:00
|
|
|
return; // skip until we get to the first one
|
|
|
|
}
|
2019-03-03 06:39:57 +00:00
|
|
|
|
2019-05-07 19:44:02 +00:00
|
|
|
// Get underlying resources of an asset - this includes files as well as details about edited PHAssets
|
2019-06-13 06:24:46 +00:00
|
|
|
NSArray<PHAssetResource *> *const assetResources = [PHAssetResource assetResourcesForAsset:asset];
|
|
|
|
if (![assetResources firstObject]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
PHAssetResource *const _Nonnull resource = [assetResources firstObject];
|
2019-03-03 06:39:57 +00:00
|
|
|
|
2019-06-13 06:24:46 +00:00
|
|
|
if ([mimeTypes count] > 0) {
|
2019-05-07 19:44:02 +00:00
|
|
|
CFStringRef const uti = (__bridge CFStringRef _Nonnull)(resource.uniformTypeIdentifier);
|
|
|
|
NSString *const mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
|
2019-03-03 06:39:57 +00:00
|
|
|
|
2019-05-07 19:44:02 +00:00
|
|
|
BOOL __block mimeTypeFound = NO;
|
|
|
|
[mimeTypes enumerateObjectsUsingBlock:^(NSString * _Nonnull mimeTypeFilter, NSUInteger idx, BOOL * _Nonnull stop) {
|
|
|
|
if ([mimeType isEqualToString:mimeTypeFilter]) {
|
|
|
|
mimeTypeFound = YES;
|
|
|
|
*stop = YES;
|
2018-12-22 08:16:33 +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 (!mimeTypeFound) {
|
2019-01-10 21:07:15 +00:00
|
|
|
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;
|
2019-06-13 06:24:46 +00:00
|
|
|
NSString *const origFilename = resource.originalFilename;
|
2019-05-07 19:44:02 +00:00
|
|
|
|
|
|
|
// A note on isStored: in the previous code that used ALAssets, isStored
|
|
|
|
// was always set to YES, probably because iCloud-synced images were never returned (?).
|
|
|
|
// To get the "isStored" information and filename, we would need to actually request the
|
|
|
|
// image data from the image manager. Those operations could get really expensive and
|
|
|
|
// would definitely utilize the disk too much.
|
|
|
|
// Thus, this field is actually not reliable.
|
|
|
|
// Note that Android also does not return the `isStored` field at all.
|
|
|
|
[assets addObject:@{
|
|
|
|
@"node": @{
|
|
|
|
@"type": assetMediaTypeLabel, // TODO: switch to mimeType?
|
|
|
|
@"group_name": currentCollectionName,
|
|
|
|
@"image": @{
|
|
|
|
@"uri": uri,
|
2019-06-13 06:24:46 +00:00
|
|
|
@"filename": origFilename,
|
2019-05-07 19:44:02 +00:00
|
|
|
@"height": @([asset pixelHeight]),
|
|
|
|
@"width": @([asset pixelWidth]),
|
|
|
|
@"isStored": @YES, // this field doesn't seem to exist on android
|
|
|
|
@"playableDuration": @([asset duration]) // fractional seconds
|
|
|
|
},
|
|
|
|
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
|
|
|
|
@"location": (loc ? @{
|
|
|
|
@"latitude": @(loc.coordinate.latitude),
|
|
|
|
@"longitude": @(loc.coordinate.longitude),
|
|
|
|
@"altitude": @(loc.altitude),
|
|
|
|
@"heading": @(loc.course),
|
|
|
|
@"speed": @(loc.speed), // speed in m/s
|
|
|
|
} : @{})
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
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
|