mirror of
https://github.com/status-im/react-native-camera-roll.git
synced 2026-08-28 20:01:12 +00:00
Fixes fix(ci): fixed android release build as a CI test step fix(android): Fix MediaStore issues and support scopped storage #322 #244 fix(android): Fix saveImageToCameraRoll #248 #254 fix(android): remove JCenter #340 #401 fix(ios): Fix performance issues #276 fix(ios): add method to get file Path from internal PH ID #135 fix(react-native): Bump used dependencies #393 ... Fixes #322 Fixes #244 Fixes #248 Fixes #254 Fixes #340 Fixes #401 Fixes #276 Fixes #135 Fixes #393 new Features feat(ios): now it's possible to listen to library selection changes `useEffect(() => { let subscription: EmitterSubscription; if (isAboveIOS14) { subscription = cameraRollEventEmitter.addListener('onLibrarySelectionChange', (_event) => { getUnloadedPictures(); }); } return () => { if (isAboveIOS14 && subscription) { subscription.remove(); } };` feat(ios): possibility to open native modal to update selection when authorization is limited iosRefreshGallerySelection() feat(ios): Convert HEIC images on demand to upload to server BREAKING CHANGE: root import changed! ``` import { CameraRoll } from @react-native-community/cameraroll instead of import CameraRoll from @react-native-community/cameraroll ``` Co-authored-by: idrissakhi <85105624+idrissakhi@users.noreply.github.com>
This commit is contained in:
co-authored by
idrissakhi
parent
3f0aed96db
commit
c230dd0074
+21
-31
@@ -1,7 +1,7 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
rn: react-native-community/react-native@4.0.4
|
||||
rn: react-native-community/react-native@7.1.1
|
||||
|
||||
jobs:
|
||||
checkout_code:
|
||||
@@ -10,8 +10,10 @@ jobs:
|
||||
- checkout
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: .
|
||||
analyze:
|
||||
paths:
|
||||
- .
|
||||
|
||||
analyse_js:
|
||||
executor: rn/linux_js
|
||||
steps:
|
||||
- attach_workspace:
|
||||
@@ -20,9 +22,6 @@ jobs:
|
||||
- run:
|
||||
name: Eslint
|
||||
command: yarn run validate:eslint
|
||||
- run:
|
||||
name: Flow
|
||||
command: yarn run validate:flow
|
||||
- run:
|
||||
name: TypeScript
|
||||
command: yarn run validate:typescript
|
||||
@@ -49,39 +48,30 @@ workflows:
|
||||
test:
|
||||
jobs:
|
||||
- checkout_code
|
||||
- analyze:
|
||||
- analyse_js:
|
||||
requires:
|
||||
- checkout_code
|
||||
- rn/android_build:
|
||||
name: android_debug_build
|
||||
project_path: "example/android"
|
||||
build_type: debug
|
||||
requires:
|
||||
- analyze
|
||||
- rn/android_build:
|
||||
name: android_release_build
|
||||
project_path: "example/android"
|
||||
build_type: release
|
||||
name: build_android_release
|
||||
project_path: android
|
||||
store_artifacts: false
|
||||
persist_to_workspace: false
|
||||
requires:
|
||||
- analyze
|
||||
# - rn/android_test:
|
||||
# logcat_grep: "com.camerarollexample"
|
||||
# detox_configuration: "android.emu.release"
|
||||
- analyse_js
|
||||
# - rn/ios_build:
|
||||
# build_configuration: Debug
|
||||
# device: iPhone 11
|
||||
# name: build_ios_debug
|
||||
# project_path: example/ios/CameraRollExample.xcworkspace
|
||||
# project_type: workspace
|
||||
# requires:
|
||||
# - android_release_build
|
||||
# - rn/ios_build_and_test:
|
||||
# project_path: "example/ios/CameraRollExample.xcodeproj"
|
||||
# derived_data_path: "example/ios/build"
|
||||
# device: "iPhone X"
|
||||
# build_configuration: "Release"
|
||||
# scheme: "CameraRollExample"
|
||||
# detox_configuration: "ios.sim.release"
|
||||
# requires:
|
||||
# - analyze
|
||||
# - analyse_js
|
||||
# scheme: CameraRollExample
|
||||
- publish:
|
||||
requires:
|
||||
- android_debug_build
|
||||
- android_release_build
|
||||
- build_android_release
|
||||
# - build_ios_debug
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
|
||||
+92
-4
@@ -1,5 +1,93 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native-community',
|
||||
};
|
||||
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
ignorePatterns: ['scripts', 'lib', 'docs', 'example', 'app.plugin.js'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: ['plugin:@typescript-eslint/recommended', '@react-native-community'],
|
||||
rules: {
|
||||
// eslint
|
||||
semi: 'off',
|
||||
curly: ['warn', 'multi-or-nest', 'consistent'],
|
||||
'no-mixed-spaces-and-tabs': ['warn', 'smart-tabs'],
|
||||
'no-async-promise-executor': 'warn',
|
||||
'require-await': 'warn',
|
||||
'no-return-await': 'warn',
|
||||
'no-await-in-loop': 'warn',
|
||||
'comma-dangle': 'off', // prettier already detects this
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'TSEnumDeclaration',
|
||||
message:
|
||||
"Enums have various disadvantages, use TypeScript's union types instead.",
|
||||
},
|
||||
],
|
||||
// prettier
|
||||
'prettier/prettier': ['warn'],
|
||||
// typescript
|
||||
'@typescript-eslint/no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
vars: 'all',
|
||||
args: 'after-used',
|
||||
ignoreRestSiblings: false,
|
||||
varsIgnorePattern: '^_',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'warn',
|
||||
{
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'error',
|
||||
// react plugin
|
||||
'react/no-unescaped-entities': 'off',
|
||||
// react native plugin
|
||||
'react-native/no-unused-styles': 'warn',
|
||||
'react-native/split-platform-components': 'off',
|
||||
'react-native/no-inline-styles': 'warn',
|
||||
'react-native/no-color-literals': 'off',
|
||||
'react-native/no-raw-text': 'off',
|
||||
'react-native/no-single-element-style-arrays': 'warn',
|
||||
'@typescript-eslint/strict-boolean-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowString: false,
|
||||
allowNullableObject: false,
|
||||
allowNumber: false,
|
||||
allowNullableBoolean: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'error',
|
||||
|
||||
// react hooks
|
||||
'react-hooks/exhaustive-deps': [
|
||||
'error',
|
||||
{
|
||||
additionalHooks:
|
||||
'(useDerivedValue|useAnimatedStyle|useAnimatedProps|useWorkletCallback|useFrameProcessor)',
|
||||
},
|
||||
],
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
globals: {
|
||||
_log: 'readonly',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -59,3 +59,6 @@ example/android-bundle.js
|
||||
example/ios-bundle.js
|
||||
index.android.bundle
|
||||
index.ios.bundle
|
||||
|
||||
# Bob generated files
|
||||
lib/
|
||||
|
||||
+1
-3
@@ -3,7 +3,5 @@
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"jsxBracketSameLine": true,
|
||||
"overrides": [
|
||||
{"files": ["*.js"], "options": {"parser": "flow", "requirePragma": true}}
|
||||
]
|
||||
"overrides": [{ "files": ["*.js"], "options": { "parser": "flow", "requirePragma": true } }]
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ import { CameraRoll } from "react-native";
|
||||
to:
|
||||
|
||||
```javascript
|
||||
import CameraRoll from "@react-native-community/cameraroll";
|
||||
import { CameraRoll } from "@react-native-community/cameraroll";
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -93,7 +93,7 @@ Then you have to explicitly ask for the permission
|
||||
|
||||
```javascript
|
||||
import { PermissionsAndroid, Platform } from "react-native";
|
||||
import CameraRoll from "@react-native-community/cameraroll";
|
||||
import { CameraRoll } from "@react-native-community/cameraroll";
|
||||
|
||||
async function hasAndroidPermission() {
|
||||
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;
|
||||
@@ -284,6 +284,138 @@ render() {
|
||||
}
|
||||
```
|
||||
|
||||
Loading images with listeners and refetchs:
|
||||
|
||||
```javascript
|
||||
import { PhotoGallery, cameraRollEventEmitter } from 'react-native-photo-gallery-api';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { AppState, EmitterSubscription } from 'react-native';
|
||||
|
||||
interface GalleryOptions {
|
||||
pageSize: number;
|
||||
mimeTypeFilter?: Array<string>;
|
||||
}
|
||||
|
||||
interface GalleryLogic {
|
||||
photos?: ImageDTO[];
|
||||
loadNextPagePictures: () => void;
|
||||
isLoading: boolean;
|
||||
isLoadingNextPage: boolean;
|
||||
isReloading: boolean;
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
|
||||
const supportedMimeTypesByTheBackEnd = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/heif',
|
||||
'image/heic',
|
||||
'image/heif-sequence',
|
||||
'image/heic-sequence',
|
||||
];
|
||||
|
||||
export const useGallery = ({
|
||||
pageSize = 30,
|
||||
mimeTypeFilter = supportedMimeTypesByTheBackEnd,
|
||||
}: GalleryOptions): GalleryLogic => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isReloading, setIsReloading] = useState(false);
|
||||
const [isLoadingNextPage, setIsLoadingNextPage] = useState(false);
|
||||
const [hasNextPage, setHasNextPage] = useState(false);
|
||||
const [nextCursor, setNextCursor] = useState<string>();
|
||||
const [photos, setPhotos] = useState<ImageDTO[]>();
|
||||
|
||||
const loadNextPagePictures = useCallback(async () => {
|
||||
try {
|
||||
nextCursor ? setIsLoadingNextPage(true) : setIsLoading(true);
|
||||
const { edges, page_info } = await PhotoGallery.getPhotos({
|
||||
first: pageSize,
|
||||
after: nextCursor,
|
||||
assetType: 'Photos',
|
||||
mimeTypes: mimeTypeFilter,
|
||||
...(isAndroid && { include: ['fileSize', 'filename'] }),
|
||||
});
|
||||
const photos = convertCameraRollPicturesToImageDtoType(edges);
|
||||
setPhotos((prev) => [...(prev ?? []), ...photos]);
|
||||
|
||||
setNextCursor(page_info.end_cursor);
|
||||
setHasNextPage(page_info.has_next_page);
|
||||
} catch (error) {
|
||||
console.error('useGallery getPhotos error:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoadingNextPage(false);
|
||||
}
|
||||
}, [mimeTypeFilter, nextCursor, pageSize]);
|
||||
|
||||
const getUnloadedPictures = useCallback(async () => {
|
||||
try {
|
||||
setIsReloading(true);
|
||||
const { edges, page_info } = await PhotoGallery.getPhotos({
|
||||
first: !photos || photos.length < pageSize ? pageSize : photos.length,
|
||||
assetType: 'Photos',
|
||||
mimeTypes: mimeTypeFilter,
|
||||
// Include fileSize only for android since it's causing performance issues on IOS.
|
||||
...(isAndroid && { include: ['fileSize', 'filename'] }),
|
||||
});
|
||||
const newPhotos = convertCameraRollPicturesToImageDtoType(edges);
|
||||
setPhotos(newPhotos);
|
||||
|
||||
setNextCursor(page_info.end_cursor);
|
||||
setHasNextPage(page_info.has_next_page);
|
||||
} catch (error) {
|
||||
console.error('useGallery getNewPhotos error:', error);
|
||||
} finally {
|
||||
setIsReloading(false);
|
||||
}
|
||||
}, [mimeTypeFilter, pageSize, photos]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!photos) {
|
||||
loadNextPagePictures();
|
||||
}
|
||||
}, [loadNextPagePictures, photos]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = AppState.addEventListener('change', async (nextAppState) => {
|
||||
if (nextAppState === 'active') {
|
||||
getUnloadedPictures();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, [getUnloadedPictures]);
|
||||
|
||||
useEffect(() => {
|
||||
let subscription: EmitterSubscription;
|
||||
if (isAboveIOS14) {
|
||||
subscription = cameraRollEventEmitter.addListener('onLibrarySelectionChange', (_event) => {
|
||||
getUnloadedPictures();
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (isAboveIOS14 && subscription) {
|
||||
subscription.remove();
|
||||
}
|
||||
};
|
||||
}, [getUnloadedPictures]);
|
||||
|
||||
return {
|
||||
photos,
|
||||
loadNextPagePictures,
|
||||
isLoading,
|
||||
isLoadingNextPage,
|
||||
isReloading,
|
||||
hasNextPage,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `deletePhotos()`
|
||||
@@ -307,6 +439,24 @@ Returns a Promise which will resolve when the deletion request is completed, or
|
||||
| uri | string | Yes | See above. |
|
||||
|
||||
|
||||
### `iosGetImageDataById()`
|
||||
```javascript
|
||||
CameraRoll.iosGetImageDataById(internalID, true);
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
| ------------ | ----------------------- | ---------- | ---------------------------------------------------- |
|
||||
| internalID | string | Yes | Ios internal ID 'PH://xxxx'. |
|
||||
| convertHeic | boolean | False | Whether to convert or not to JPEG image. |
|
||||
|
||||
### Known issues
|
||||
|
||||
#### IOS
|
||||
|
||||
If you try to save media into specific album without asking for read and write permission then saving will not work, workaround is to not precice album name for IOS if you don't want to request full permission (Only ios >= 14).
|
||||
|
||||
[circle-ci-badge]:https://img.shields.io/circleci/project/github/react-native-cameraroll/react-native-cameraroll/master.svg?style=flat-square
|
||||
[circle-ci]:https://circleci.com/gh/react-native-cameraroll/workflows/react-native-cameraroll/tree/master
|
||||
[supported-os-badge]:https://img.shields.io/badge/platforms-android%20|%20ios-lightgrey.svg?style=flat-square
|
||||
|
||||
+44
-11
@@ -1,15 +1,36 @@
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
// The Android Gradle plugin is only required when opening the android folder stand-alone.
|
||||
// This avoids unnecessary downloads and potential conflicts when the library is included as a
|
||||
// module dependency in an application project.
|
||||
if (project == rootProject) {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:4.2.2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task downloadDependencies() {
|
||||
description 'Download all dependencies to the Gradle cache'
|
||||
doLast {
|
||||
configurations.findAll().each { config ->
|
||||
if (config.name.contains("minReactNative") && config.canBeResolved) {
|
||||
print config.name
|
||||
print '\n'
|
||||
config.files
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getExtOrInitialValue(name, initialValue) {
|
||||
return rootProject.ext.has(name) ? rootProject.ext.get(name) : initialValue
|
||||
}
|
||||
|
||||
def getExtOrDefault(name) {
|
||||
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ReactNativeCameraRoll_' + name]
|
||||
}
|
||||
@@ -22,21 +43,33 @@ apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
|
||||
buildToolsVersion getExtOrDefault('buildToolsVersion')
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion getExtOrIntegerDefault('minSdkVersion')
|
||||
targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
|
||||
}
|
||||
lintOptions{
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url "$rootDir/../node_modules/react-native/android"
|
||||
}
|
||||
google()
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//noinspection GradleDynamicVersion
|
||||
api 'com.facebook.react:react-native:+'
|
||||
}
|
||||
implementation 'com.facebook.react:react-native:+'
|
||||
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
ReactNativeCameraRoll_compileSdkVersion=28
|
||||
ReactNativeCameraRoll_buildToolsVersion=28.0.3
|
||||
ReactNativeCameraRoll_targetSdkVersion=27
|
||||
ReactNativeCameraRoll_minSdkVersion=16
|
||||
ReactNativeCameraRoll_compileSdkVersion=31
|
||||
ReactNativeCameraRoll_buildToolsVersion=29.0.2
|
||||
ReactNativeCameraRoll_targetSdkVersion=31
|
||||
ReactNativeCameraRoll_minSdkVersion=23
|
||||
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -9,6 +9,7 @@ package com.reactnativecommunity.cameraroll;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
@@ -18,7 +19,9 @@ import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.FileUtils;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.MediaStore.Images;
|
||||
import android.text.TextUtils;
|
||||
@@ -46,7 +49,7 @@ import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -82,20 +85,19 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
private static final String INCLUDE_PLAYABLE_DURATION = "playableDuration";
|
||||
|
||||
private static final String[] PROJECTION = {
|
||||
Images.Media._ID,
|
||||
Images.Media.MIME_TYPE,
|
||||
Images.Media.BUCKET_DISPLAY_NAME,
|
||||
Images.Media.DATE_TAKEN,
|
||||
MediaStore.MediaColumns.DATE_ADDED,
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
MediaStore.MediaColumns.WIDTH,
|
||||
MediaStore.MediaColumns.HEIGHT,
|
||||
MediaStore.MediaColumns.SIZE,
|
||||
MediaStore.MediaColumns.DATA
|
||||
Images.Media._ID,
|
||||
Images.Media.MIME_TYPE,
|
||||
Images.Media.BUCKET_DISPLAY_NAME,
|
||||
Images.Media.DATE_TAKEN,
|
||||
MediaStore.MediaColumns.DATE_ADDED,
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
MediaStore.MediaColumns.WIDTH,
|
||||
MediaStore.MediaColumns.HEIGHT,
|
||||
MediaStore.MediaColumns.SIZE,
|
||||
MediaStore.MediaColumns.DATA
|
||||
};
|
||||
|
||||
private static final String SELECTION_BUCKET = Images.Media.BUCKET_DISPLAY_NAME + " = ?";
|
||||
private static final String SELECTION_DATE_TAKEN = Images.Media.DATE_TAKEN + " < ?";
|
||||
|
||||
public CameraRollModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
@@ -111,13 +113,13 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
* from wherever it may be to the external storage pictures directory, so that it can be scanned
|
||||
* by the MediaScanner.
|
||||
*
|
||||
* @param uri the file:// URI of the image to save
|
||||
* @param uri the file:// URI of the image to save
|
||||
* @param promise to be resolved or rejected
|
||||
*/
|
||||
@ReactMethod
|
||||
public void saveToCameraRoll(String uri, ReadableMap options, Promise promise) {
|
||||
new SaveToCameraRoll(getReactApplicationContext(), Uri.parse(uri), options, promise)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private static class SaveToCameraRoll extends GuardedAsyncTask<Void, Void> {
|
||||
@@ -138,82 +140,107 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
@Override
|
||||
protected void doInBackgroundGuarded(Void... params) {
|
||||
File source = new File(mUri.getPath());
|
||||
FileChannel input = null, output = null;
|
||||
try {
|
||||
boolean isAlbumPresent = !"".equals(mOptions.getString("album"));
|
||||
|
||||
final File environment;
|
||||
// Media is not saved into an album when using Environment.DIRECTORY_DCIM.
|
||||
if (isAlbumPresent) {
|
||||
if ("video".equals(mOptions.getString("type"))) {
|
||||
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||
} else {
|
||||
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
}
|
||||
} else {
|
||||
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
|
||||
}
|
||||
FileInputStream input = null;
|
||||
OutputStream output = null;
|
||||
|
||||
File exportDir;
|
||||
if (isAlbumPresent) {
|
||||
exportDir = new File(environment, mOptions.getString("album"));
|
||||
if (!exportDir.exists() && !exportDir.mkdirs()) {
|
||||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Album Directory not created. Did you request WRITE_EXTERNAL_STORAGE?");
|
||||
String mimeType = Utils.getMimeType(mUri.getPath());
|
||||
|
||||
try {
|
||||
String album = mOptions.getString("album");
|
||||
boolean isAlbumPresent = !TextUtils.isEmpty(album);
|
||||
|
||||
// Android Q and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ContentValues mediaDetails = new ContentValues();
|
||||
if (isAlbumPresent) {
|
||||
String relativePath = Environment.DIRECTORY_DCIM + File.separator + album;
|
||||
mediaDetails.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath);
|
||||
}
|
||||
mediaDetails.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
|
||||
mediaDetails.put(Images.Media.DISPLAY_NAME, source.getName());
|
||||
mediaDetails.put(Images.Media.IS_PENDING, 1);
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
Uri mediaContentUri = resolver
|
||||
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mediaDetails);
|
||||
output = resolver.openOutputStream(mediaContentUri);
|
||||
input = new FileInputStream(source);
|
||||
FileUtils.copy(input, output);
|
||||
mediaDetails.clear();
|
||||
mediaDetails.put(Images.Media.IS_PENDING, 0);
|
||||
resolver.update(mediaContentUri, mediaDetails, null, null);
|
||||
mPromise.resolve(mediaContentUri.toString());
|
||||
} else {
|
||||
final File environment;
|
||||
// Media is not saved into an album when using Environment.DIRECTORY_DCIM.
|
||||
if (isAlbumPresent) {
|
||||
if ("video".equals(mOptions.getString("type"))) {
|
||||
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES);
|
||||
} else {
|
||||
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
}
|
||||
} else {
|
||||
environment = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
|
||||
}
|
||||
File exportDir;
|
||||
if (isAlbumPresent) {
|
||||
exportDir = new File(environment, album);
|
||||
if (!exportDir.exists() && !exportDir.mkdirs()) {
|
||||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Album Directory not created. Did you request WRITE_EXTERNAL_STORAGE?");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
exportDir = environment;
|
||||
}
|
||||
|
||||
if (!exportDir.isDirectory()) {
|
||||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
exportDir = environment;
|
||||
}
|
||||
|
||||
if (!exportDir.isDirectory()) {
|
||||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "External media storage directory not available");
|
||||
return;
|
||||
}
|
||||
File dest = new File(exportDir, source.getName());
|
||||
int n = 0;
|
||||
String fullSourceName = source.getName();
|
||||
String sourceName, sourceExt;
|
||||
if (fullSourceName.indexOf('.') >= 0) {
|
||||
sourceName = fullSourceName.substring(0, fullSourceName.lastIndexOf('.'));
|
||||
sourceExt = fullSourceName.substring(fullSourceName.lastIndexOf('.'));
|
||||
} else {
|
||||
sourceName = fullSourceName;
|
||||
sourceExt = "";
|
||||
}
|
||||
while (!dest.createNewFile()) {
|
||||
dest = new File(exportDir, sourceName + "_" + (n++) + sourceExt);
|
||||
}
|
||||
input = new FileInputStream(source).getChannel();
|
||||
output = new FileOutputStream(dest).getChannel();
|
||||
output.transferFrom(input, 0, input.size());
|
||||
input.close();
|
||||
output.close();
|
||||
File dest = new File(exportDir, source.getName());
|
||||
int n = 0;
|
||||
String fullSourceName = source.getName();
|
||||
String sourceName, sourceExt;
|
||||
if (fullSourceName.indexOf('.') >= 0) {
|
||||
sourceName = fullSourceName.substring(0, fullSourceName.lastIndexOf('.'));
|
||||
sourceExt = fullSourceName.substring(fullSourceName.lastIndexOf('.'));
|
||||
} else {
|
||||
sourceName = fullSourceName;
|
||||
sourceExt = "";
|
||||
}
|
||||
while (!dest.createNewFile()) {
|
||||
dest = new File(exportDir, sourceName + "_" + (n++) + sourceExt);
|
||||
}
|
||||
input = new FileInputStream(source);
|
||||
output = new FileOutputStream(dest);
|
||||
((FileOutputStream) output).getChannel()
|
||||
.transferFrom(input.getChannel(), 0, input.getChannel().size());
|
||||
input.close();
|
||||
output.close();
|
||||
|
||||
MediaScannerConnection.scanFile(
|
||||
mContext,
|
||||
new String[]{dest.getAbsolutePath()},
|
||||
null,
|
||||
new MediaScannerConnection.OnScanCompletedListener() {
|
||||
@Override
|
||||
public void onScanCompleted(String path, Uri uri) {
|
||||
if (uri != null) {
|
||||
mPromise.resolve(uri.toString());
|
||||
} else {
|
||||
mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery");
|
||||
}
|
||||
}
|
||||
});
|
||||
MediaScannerConnection.scanFile(
|
||||
mContext,
|
||||
new String[]{dest.getAbsolutePath()},
|
||||
null,
|
||||
(path, uri) -> {
|
||||
if (uri != null) {
|
||||
mPromise.resolve(uri.toString());
|
||||
} else {
|
||||
mPromise.reject(ERROR_UNABLE_TO_SAVE, "Could not add image to gallery");
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (IOException e) {
|
||||
mPromise.reject(e);
|
||||
} finally {
|
||||
if (input != null && input.isOpen()) {
|
||||
if (input != null) {
|
||||
try {
|
||||
input.close();
|
||||
} catch (IOException e) {
|
||||
FLog.e(ReactConstants.TAG, "Could not close input channel", e);
|
||||
}
|
||||
}
|
||||
if (output != null && output.isOpen()) {
|
||||
if (output != null) {
|
||||
try {
|
||||
output.close();
|
||||
} catch (IOException e) {
|
||||
@@ -227,25 +254,25 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
/**
|
||||
* Get photos from {@link MediaStore.Images}, most recent first.
|
||||
*
|
||||
* @param params a map containing the following keys:
|
||||
* <ul>
|
||||
* <li>first (mandatory): a number representing the number of photos to fetch</li>
|
||||
* <li>
|
||||
* after (optional): a cursor that matches page_info[end_cursor] returned by a
|
||||
* previous call to {@link #getPhotos}
|
||||
* </li>
|
||||
* <li>groupName (optional): an album name</li>
|
||||
* <li>
|
||||
* mimeType (optional): restrict returned images to a specific mimetype (e.g.
|
||||
* image/jpeg)
|
||||
* </li>
|
||||
* <li>
|
||||
* assetType (optional): chooses between either photos or videos from the camera roll.
|
||||
* Valid values are "Photos" or "Videos". Defaults to photos.
|
||||
* </li>
|
||||
* </ul>
|
||||
* @param params a map containing the following keys:
|
||||
* <ul>
|
||||
* <li>first (mandatory): a number representing the number of photos to fetch</li>
|
||||
* <li>
|
||||
* after (optional): a cursor that matches page_info[end_cursor] returned by a
|
||||
* previous call to {@link #getPhotos}
|
||||
* </li>
|
||||
* <li>groupName (optional): an album name</li>
|
||||
* <li>
|
||||
* mimeType (optional): restrict returned images to a specific mimetype (e.g.
|
||||
* image/jpeg)
|
||||
* </li>
|
||||
* <li>
|
||||
* assetType (optional): chooses between either photos or videos from the camera roll.
|
||||
* Valid values are "Photos" or "Videos". Defaults to photos.
|
||||
* </li>
|
||||
* </ul>
|
||||
* @param promise the Promise to be resolved when the photos are loaded; for a format of the
|
||||
* parameters passed to this callback, see {@code getPhotosReturnChecker} in CameraRoll.js
|
||||
* parameters passed to this callback, see {@code getPhotosReturnChecker} in CameraRoll.js
|
||||
*/
|
||||
@ReactMethod
|
||||
public void getPhotos(final ReadableMap params, final Promise promise) {
|
||||
@@ -256,30 +283,33 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
long fromTime = params.hasKey("fromTime") ? (long) params.getDouble("fromTime") : 0;
|
||||
long toTime = params.hasKey("toTime") ? (long) params.getDouble("toTime") : 0;
|
||||
ReadableArray mimeTypes = params.hasKey("mimeTypes")
|
||||
? params.getArray("mimeTypes")
|
||||
: null;
|
||||
? params.getArray("mimeTypes")
|
||||
: null;
|
||||
ReadableArray include = params.hasKey("include") ? params.getArray("include") : null;
|
||||
|
||||
new GetMediaTask(
|
||||
getReactApplicationContext(),
|
||||
first,
|
||||
after,
|
||||
groupName,
|
||||
mimeTypes,
|
||||
assetType,
|
||||
fromTime,
|
||||
toTime,
|
||||
include,
|
||||
promise)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
getReactApplicationContext(),
|
||||
first,
|
||||
after,
|
||||
groupName,
|
||||
mimeTypes,
|
||||
assetType,
|
||||
fromTime,
|
||||
toTime,
|
||||
include,
|
||||
promise)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private static class GetMediaTask extends GuardedAsyncTask<Void, Void> {
|
||||
private final Context mContext;
|
||||
private final int mFirst;
|
||||
private final @Nullable String mAfter;
|
||||
private final @Nullable String mGroupName;
|
||||
private final @Nullable ReadableArray mMimeTypes;
|
||||
private final @Nullable
|
||||
String mAfter;
|
||||
private final @Nullable
|
||||
String mGroupName;
|
||||
private final @Nullable
|
||||
ReadableArray mMimeTypes;
|
||||
private final Promise mPromise;
|
||||
private final String mAssetType;
|
||||
private final long mFromTime;
|
||||
@@ -287,16 +317,16 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
private final Set<String> mInclude;
|
||||
|
||||
private GetMediaTask(
|
||||
ReactContext context,
|
||||
int first,
|
||||
@Nullable String after,
|
||||
@Nullable String groupName,
|
||||
@Nullable ReadableArray mimeTypes,
|
||||
String assetType,
|
||||
long fromTime,
|
||||
long toTime,
|
||||
@Nullable ReadableArray include,
|
||||
Promise promise) {
|
||||
ReactContext context,
|
||||
int first,
|
||||
@Nullable String after,
|
||||
@Nullable String groupName,
|
||||
@Nullable ReadableArray mimeTypes,
|
||||
String assetType,
|
||||
long fromTime,
|
||||
long toTime,
|
||||
@Nullable ReadableArray include,
|
||||
Promise promise) {
|
||||
super(context);
|
||||
mContext = context;
|
||||
mFirst = first;
|
||||
@@ -338,19 +368,19 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
|
||||
if (mAssetType.equals(ASSET_TYPE_PHOTOS)) {
|
||||
selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " = "
|
||||
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
|
||||
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
|
||||
} else if (mAssetType.equals(ASSET_TYPE_VIDEOS)) {
|
||||
selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " = "
|
||||
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
|
||||
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
|
||||
} else if (mAssetType.equals(ASSET_TYPE_ALL)) {
|
||||
selection.append(" AND " + MediaStore.Files.FileColumns.MEDIA_TYPE + " IN ("
|
||||
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO + ","
|
||||
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE + ")");
|
||||
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO + ","
|
||||
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE + ")");
|
||||
} else {
|
||||
mPromise.reject(
|
||||
ERROR_UNABLE_TO_FILTER,
|
||||
"Invalid filter option: '" + mAssetType + "'. Expected one of '"
|
||||
+ ASSET_TYPE_PHOTOS + "', '" + ASSET_TYPE_VIDEOS + "' or '" + ASSET_TYPE_ALL + "'."
|
||||
ERROR_UNABLE_TO_FILTER,
|
||||
"Invalid filter option: '" + mAssetType + "'. Expected one of '"
|
||||
+ ASSET_TYPE_PHOTOS + "', '" + ASSET_TYPE_VIDEOS + "' or '" + ASSET_TYPE_ALL + "'."
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -378,19 +408,36 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
|
||||
try {
|
||||
// set LIMIT to first + 1 so that we know how to populate page_info
|
||||
String limit = "limit=" + (mFirst + 1);
|
||||
|
||||
if (!TextUtils.isEmpty(mAfter)) {
|
||||
limit = "limit=" + mAfter + "," + (mFirst + 1);
|
||||
Cursor media;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection.toString());
|
||||
bundle.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
|
||||
selectionArgs.toArray(new String[selectionArgs.size()]));
|
||||
bundle.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER, Images.Media.DATE_ADDED + " DESC, " + Images.Media.DATE_MODIFIED + " DESC");
|
||||
bundle.putInt(ContentResolver.QUERY_ARG_LIMIT, mFirst + 1);
|
||||
if (!TextUtils.isEmpty(mAfter)) {
|
||||
bundle.putInt(ContentResolver.QUERY_ARG_OFFSET, Integer.parseInt(mAfter));
|
||||
}
|
||||
media = resolver.query(
|
||||
MediaStore.Files.getContentUri("external"),
|
||||
PROJECTION,
|
||||
bundle,
|
||||
null);
|
||||
} else {
|
||||
// set LIMIT to first + 1 so that we know how to populate page_info
|
||||
String limit = "limit=" + (mFirst + 1);
|
||||
if (!TextUtils.isEmpty(mAfter)) {
|
||||
limit = "limit=" + mAfter + "," + (mFirst + 1);
|
||||
}
|
||||
media = resolver.query(
|
||||
MediaStore.Files.getContentUri("external").buildUpon().encodedQuery(limit).build(),
|
||||
PROJECTION,
|
||||
selection.toString(),
|
||||
selectionArgs.toArray(new String[selectionArgs.size()]),
|
||||
Images.Media.DATE_ADDED + " DESC, " + Images.Media.DATE_MODIFIED + " DESC");
|
||||
}
|
||||
|
||||
Cursor media = resolver.query(
|
||||
MediaStore.Files.getContentUri("external").buildUpon().encodedQuery(limit).build(),
|
||||
PROJECTION,
|
||||
selection.toString(),
|
||||
selectionArgs.toArray(new String[selectionArgs.size()]),
|
||||
Images.Media.DATE_ADDED + " DESC, " + Images.Media.DATE_MODIFIED + " DESC");
|
||||
if (media == null) {
|
||||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get media");
|
||||
} else {
|
||||
@@ -404,9 +451,9 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
mPromise.reject(
|
||||
ERROR_UNABLE_TO_LOAD_PERMISSION,
|
||||
"Could not get media: need READ_EXTERNAL_STORAGE permission",
|
||||
e);
|
||||
ERROR_UNABLE_TO_LOAD_PERMISSION,
|
||||
"Could not get media: need READ_EXTERNAL_STORAGE permission",
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,7 +500,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
Map<String, Integer> albums = new HashMap<>();
|
||||
do {
|
||||
int column = media.getColumnIndex(MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME);
|
||||
if ( column < 0 ) {
|
||||
if (column < 0) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
String albumName = media.getString(column);
|
||||
@@ -489,19 +536,19 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
pageInfo.putBoolean("has_next_page", limit < media.getCount());
|
||||
if (limit < media.getCount()) {
|
||||
pageInfo.putString(
|
||||
"end_cursor",
|
||||
Integer.toString(offset + limit)
|
||||
"end_cursor",
|
||||
Integer.toString(offset + limit)
|
||||
);
|
||||
}
|
||||
response.putMap("page_info", pageInfo);
|
||||
}
|
||||
|
||||
private static void putEdges(
|
||||
ContentResolver resolver,
|
||||
Cursor media,
|
||||
WritableMap response,
|
||||
int limit,
|
||||
Set<String> include) {
|
||||
ContentResolver resolver,
|
||||
Cursor media,
|
||||
WritableMap response,
|
||||
int limit,
|
||||
Set<String> include) {
|
||||
WritableArray edges = new WritableNativeArray();
|
||||
media.moveToFirst();
|
||||
int mimeTypeIndex = media.getColumnIndex(Images.Media.MIME_TYPE);
|
||||
@@ -524,9 +571,9 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
WritableMap edge = new WritableNativeMap();
|
||||
WritableMap node = new WritableNativeMap();
|
||||
boolean imageInfoSuccess =
|
||||
putImageInfo(resolver, media, node, widthIndex, heightIndex, sizeIndex, dataIndex,
|
||||
mimeTypeIndex, includeFilename, includeFileSize, includeImageSize,
|
||||
includePlayableDuration);
|
||||
putImageInfo(resolver, media, node, widthIndex, heightIndex, sizeIndex, dataIndex,
|
||||
mimeTypeIndex, includeFilename, includeFileSize, includeImageSize,
|
||||
includePlayableDuration);
|
||||
if (imageInfoSuccess) {
|
||||
putBasicNodeInfo(media, node, mimeTypeIndex, groupNameIndex, dateTakenIndex, dateAddedIndex, dateModifiedIndex);
|
||||
putLocationInfo(media, node, dataIndex, includeLocation);
|
||||
@@ -544,19 +591,19 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
private static void putBasicNodeInfo(
|
||||
Cursor media,
|
||||
WritableMap node,
|
||||
int mimeTypeIndex,
|
||||
int groupNameIndex,
|
||||
int dateTakenIndex,
|
||||
int dateAddedIndex,
|
||||
int dateModifiedIndex) {
|
||||
Cursor media,
|
||||
WritableMap node,
|
||||
int mimeTypeIndex,
|
||||
int groupNameIndex,
|
||||
int dateTakenIndex,
|
||||
int dateAddedIndex,
|
||||
int dateModifiedIndex) {
|
||||
node.putString("type", media.getString(mimeTypeIndex));
|
||||
node.putString("group_name", media.getString(groupNameIndex));
|
||||
long dateTaken = media.getLong(dateTakenIndex);
|
||||
if (dateTaken == 0L) {
|
||||
//date added is in seconds, date taken in milliseconds, thus the multiplication
|
||||
dateTaken = media.getLong(dateAddedIndex) * 1000;
|
||||
//date added is in seconds, date taken in milliseconds, thus the multiplication
|
||||
dateTaken = media.getLong(dateAddedIndex) * 1000;
|
||||
}
|
||||
node.putDouble("timestamp", dateTaken / 1000d);
|
||||
node.putDouble("modified", media.getLong(dateModifiedIndex));
|
||||
@@ -567,18 +614,18 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
* to include
|
||||
*/
|
||||
private static boolean putImageInfo(
|
||||
ContentResolver resolver,
|
||||
Cursor media,
|
||||
WritableMap node,
|
||||
int widthIndex,
|
||||
int heightIndex,
|
||||
int sizeIndex,
|
||||
int dataIndex,
|
||||
int mimeTypeIndex,
|
||||
boolean includeFilename,
|
||||
boolean includeFileSize,
|
||||
boolean includeImageSize,
|
||||
boolean includePlayableDuration) {
|
||||
ContentResolver resolver,
|
||||
Cursor media,
|
||||
WritableMap node,
|
||||
int widthIndex,
|
||||
int heightIndex,
|
||||
int sizeIndex,
|
||||
int dataIndex,
|
||||
int mimeTypeIndex,
|
||||
boolean includeFilename,
|
||||
boolean includeFileSize,
|
||||
boolean includeImageSize,
|
||||
boolean includePlayableDuration) {
|
||||
WritableMap image = new WritableNativeMap();
|
||||
Uri photoUri = Uri.parse("file://" + media.getString(dataIndex));
|
||||
image.putString("uri", photoUri.toString());
|
||||
@@ -586,9 +633,9 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
|
||||
boolean isVideo = mimeType != null && mimeType.startsWith("video");
|
||||
boolean putImageSizeSuccess = putImageSize(resolver, media, image, widthIndex, heightIndex,
|
||||
photoUri, isVideo, includeImageSize);
|
||||
photoUri, isVideo, includeImageSize);
|
||||
boolean putPlayableDurationSuccess = putPlayableDuration(resolver, image, photoUri, isVideo,
|
||||
includePlayableDuration);
|
||||
includePlayableDuration);
|
||||
|
||||
if (includeFilename) {
|
||||
File file = new File(media.getString(dataIndex));
|
||||
@@ -612,11 +659,11 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
* @return Whether we succeeded in fetching and putting the playableDuration
|
||||
*/
|
||||
private static boolean putPlayableDuration(
|
||||
ContentResolver resolver,
|
||||
WritableMap image,
|
||||
Uri photoUri,
|
||||
boolean isVideo,
|
||||
boolean includePlayableDuration) {
|
||||
ContentResolver resolver,
|
||||
WritableMap image,
|
||||
Uri photoUri,
|
||||
boolean isVideo,
|
||||
boolean includePlayableDuration) {
|
||||
image.putNull("playableDuration");
|
||||
|
||||
if (!includePlayableDuration || !isVideo) {
|
||||
@@ -641,15 +688,15 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
// Do nothing. We can't handle this, and this is usually a system problem
|
||||
}
|
||||
try {
|
||||
int timeInMillisec = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
|
||||
playableDuration = timeInMillisec / 1000;
|
||||
int timeInMillisecond = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
|
||||
playableDuration = timeInMillisecond / 1000;
|
||||
} catch (NumberFormatException e) {
|
||||
success = false;
|
||||
FLog.e(
|
||||
ReactConstants.TAG,
|
||||
"Number format exception occurred while trying to fetch video metadata for "
|
||||
+ photoUri.toString(),
|
||||
e);
|
||||
ReactConstants.TAG,
|
||||
"Number format exception occurred while trying to fetch video metadata for "
|
||||
+ photoUri.toString(),
|
||||
e);
|
||||
}
|
||||
retriever.release();
|
||||
}
|
||||
@@ -670,14 +717,14 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
private static boolean putImageSize(
|
||||
ContentResolver resolver,
|
||||
Cursor media,
|
||||
WritableMap image,
|
||||
int widthIndex,
|
||||
int heightIndex,
|
||||
Uri photoUri,
|
||||
boolean isVideo,
|
||||
boolean includeImageSize) {
|
||||
ContentResolver resolver,
|
||||
Cursor media,
|
||||
WritableMap image,
|
||||
int widthIndex,
|
||||
int heightIndex,
|
||||
Uri photoUri,
|
||||
boolean isVideo,
|
||||
boolean includeImageSize) {
|
||||
image.putNull("width");
|
||||
image.putNull("height");
|
||||
|
||||
@@ -686,20 +733,24 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
boolean success = true;
|
||||
@Nullable AssetFileDescriptor photoDescriptor = null;
|
||||
|
||||
/* Read height and width data from the gallery cursor columns */
|
||||
int width = media.getInt(widthIndex);
|
||||
int height = media.getInt(heightIndex);
|
||||
|
||||
/* If the columns don't contain the size information, read the media file */
|
||||
if (width <= 0 || height <= 0) {
|
||||
@Nullable AssetFileDescriptor mediaDescriptor = null;
|
||||
try {
|
||||
photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
|
||||
mediaDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
|
||||
} catch (FileNotFoundException e) {
|
||||
success = false;
|
||||
FLog.e(ReactConstants.TAG, "Could not open asset file " + photoUri.toString(), e);
|
||||
}
|
||||
if (mediaDescriptor != null) {
|
||||
if (isVideo) {
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
try {
|
||||
retriever.setDataSource(photoDescriptor.getFileDescriptor());
|
||||
retriever.setDataSource(mediaDescriptor.getFileDescriptor());
|
||||
} catch (RuntimeException e) {
|
||||
// Do nothing. We can't handle this, and this is usually a system problem
|
||||
}
|
||||
@@ -709,53 +760,33 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
} catch (NumberFormatException e) {
|
||||
success = false;
|
||||
FLog.e(
|
||||
ReactConstants.TAG,
|
||||
"Number format exception occurred while trying to fetch video metadata for "
|
||||
+ photoUri.toString(),
|
||||
e);
|
||||
ReactConstants.TAG,
|
||||
"Number format exception occurred while trying to fetch video metadata for "
|
||||
+ photoUri.toString(),
|
||||
e);
|
||||
}
|
||||
retriever.release();
|
||||
} else {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
// Set inJustDecodeBounds to true so we don't actually load the Bitmap in memory,
|
||||
// but only get its dimensions
|
||||
// Set inJustDecodeBounds to true so we don't actually load the Bitmap, but only get its
|
||||
// dimensions instead.
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFileDescriptor(photoDescriptor.getFileDescriptor(), null, options);
|
||||
BitmapFactory.decodeFileDescriptor(mediaDescriptor.getFileDescriptor(), null, options);
|
||||
width = options.outWidth;
|
||||
height = options.outHeight;
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
success = false;
|
||||
FLog.e(ReactConstants.TAG, "Could not open asset file " + photoUri.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/* Read the EXIF photo data to update height and width in case a rotation is encoded */
|
||||
if (success && !isVideo && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
try {
|
||||
if (photoDescriptor == null) photoDescriptor = resolver.openAssetFileDescriptor(photoUri, "r");
|
||||
ExifInterface exif = new ExifInterface(photoDescriptor.getFileDescriptor());
|
||||
int rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
if (rotation == ExifInterface.ORIENTATION_ROTATE_90 || rotation == ExifInterface.ORIENTATION_ROTATE_270) {
|
||||
// swap values
|
||||
int temp = width;
|
||||
width = height;
|
||||
height = temp;
|
||||
try {
|
||||
mediaDescriptor.close();
|
||||
} catch (IOException e) {
|
||||
FLog.e(
|
||||
ReactConstants.TAG,
|
||||
"Can't close media descriptor "
|
||||
+ photoUri.toString(),
|
||||
e);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
success = false;
|
||||
FLog.e(ReactConstants.TAG, "Could not open asset file " + photoUri.toString(), e);
|
||||
} catch (IOException e) {
|
||||
FLog.e(ReactConstants.TAG, "Could not get exif data for file " + photoUri.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (photoDescriptor != null) {
|
||||
try {
|
||||
photoDescriptor.close();
|
||||
} catch (IOException e) {
|
||||
// Do nothing. We can't handle this, and this is usually a system problem
|
||||
}
|
||||
}
|
||||
|
||||
image.putInt("width", width);
|
||||
@@ -764,40 +795,40 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
private static void putLocationInfo(
|
||||
Cursor media,
|
||||
WritableMap node,
|
||||
int dataIndex,
|
||||
boolean includeLocation) {
|
||||
Cursor media,
|
||||
WritableMap node,
|
||||
int dataIndex,
|
||||
boolean includeLocation) {
|
||||
node.putNull("location");
|
||||
|
||||
if (!includeLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// location details are no longer indexed for privacy reasons using string Media.LATITUDE, Media.LONGITUDE
|
||||
// we manually obtain location metadata using ExifInterface#getLatLong(float[]).
|
||||
// ExifInterface is added in API level 5
|
||||
final ExifInterface exif = new ExifInterface(media.getString(dataIndex));
|
||||
float[] imageCoordinates = new float[2];
|
||||
boolean hasCoordinates = exif.getLatLong(imageCoordinates);
|
||||
if (hasCoordinates) {
|
||||
double longitude = imageCoordinates[1];
|
||||
double latitude = imageCoordinates[0];
|
||||
WritableMap location = new WritableNativeMap();
|
||||
location.putDouble("longitude", longitude);
|
||||
location.putDouble("latitude", latitude);
|
||||
node.putMap("location", location);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
FLog.e(ReactConstants.TAG, "Could not read the metadata", e);
|
||||
try {
|
||||
// location details are no longer indexed for privacy reasons using string Media.LATITUDE, Media.LONGITUDE
|
||||
// we manually obtain location metadata using ExifInterface#getLatLong(float[]).
|
||||
// ExifInterface is added in API level 5
|
||||
final ExifInterface exif = new ExifInterface(media.getString(dataIndex));
|
||||
float[] imageCoordinates = new float[2];
|
||||
boolean hasCoordinates = exif.getLatLong(imageCoordinates);
|
||||
if (hasCoordinates) {
|
||||
double longitude = imageCoordinates[1];
|
||||
double latitude = imageCoordinates[0];
|
||||
WritableMap location = new WritableNativeMap();
|
||||
location.putDouble("longitude", longitude);
|
||||
location.putDouble("latitude", latitude);
|
||||
node.putMap("location", location);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
FLog.e(ReactConstants.TAG, "Could not read the metadata", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a set of images.
|
||||
*
|
||||
* @param uris array of file:// URIs of the images to delete
|
||||
* @param uris array of file:// URIs of the images to delete
|
||||
* @param promise to be resolved
|
||||
*/
|
||||
@ReactMethod
|
||||
@@ -806,7 +837,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
promise.reject(ERROR_UNABLE_TO_DELETE, "Need at least one URI to delete");
|
||||
} else {
|
||||
new DeletePhotos(getReactApplicationContext(), uris, promise)
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,7 +859,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
ContentResolver resolver = mContext.getContentResolver();
|
||||
|
||||
// Set up the projection (we only need the ID)
|
||||
String[] projection = { MediaStore.Images.Media._ID };
|
||||
String[] projection = {MediaStore.Images.Media._ID};
|
||||
|
||||
// Match on the file path
|
||||
String innerWhere = "?";
|
||||
@@ -864,7 +895,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||
mPromise.resolve(true);
|
||||
} else {
|
||||
mPromise.reject(ERROR_UNABLE_TO_DELETE,
|
||||
"Could not delete all media, only deleted " + deletedCount + " photos.");
|
||||
"Could not delete all media, only deleted " + deletedCount + " photos.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.reactnativecommunity.cameraroll;
|
||||
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
public class Utils {
|
||||
|
||||
public static String getMimeType(String url) {
|
||||
String type = null;
|
||||
String extension = MimeTypeMap.getFileExtensionFromUrl(url);
|
||||
if (extension != null) {
|
||||
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
}
|
||||
+4
-4
@@ -1,13 +1,13 @@
|
||||
module.exports = {
|
||||
presets: ["module:metro-react-native-babel-preset"],
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
plugins: [
|
||||
[
|
||||
"module-resolver",
|
||||
'module-resolver',
|
||||
{
|
||||
alias: {
|
||||
"@react-native-community/cameraroll": "./js/CameraRoll.js"
|
||||
'@react-native-community/cameraroll': './src/index.ts'
|
||||
},
|
||||
cwd: "babelrc"
|
||||
cwd: 'babelrc'
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -9,18 +9,16 @@ import com.android.build.OutputFile
|
||||
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
|
||||
* bundle directly from the development server. Below you can see all the possible configurations
|
||||
* and their defaults. If you decide to add a configuration block, make sure to add it before the
|
||||
* `apply from: "../../../node_modules/react-native/react.gradle"` line.
|
||||
* `apply from: "../../node_modules/react-native/react.gradle"` line.
|
||||
*
|
||||
* project.ext.react = [
|
||||
* // the name of the generated asset file containing your JS bundle
|
||||
* bundleAssetName: "index.android.bundle",
|
||||
*
|
||||
* // the entry file for bundle generation. If none specified and
|
||||
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
|
||||
* // default. Can be overridden with ENTRY_FILE environment variable.
|
||||
* // the entry file for bundle generation
|
||||
* entryFile: "index.android.js",
|
||||
*
|
||||
* // https://reactnative.dev/docs/performance#enable-the-ram-format
|
||||
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
|
||||
* bundleCommand: "ram-bundle",
|
||||
*
|
||||
* // whether to bundle JS and assets in debug mode
|
||||
@@ -78,11 +76,9 @@ import com.android.build.OutputFile
|
||||
*/
|
||||
|
||||
project.ext.react = [
|
||||
// Start added for this project (react-native-cameraroll)
|
||||
cliPath: "node_modules/react-native/local-cli/cli.js",
|
||||
entryFile: "example/index.js",
|
||||
cliPath: "../../../node_modules/react-native/local-cli/cli.js",
|
||||
entryFile: "example/index.tsx",
|
||||
root: "../../../",
|
||||
// End added
|
||||
enableHermes: false, // clean and rebuild if changing
|
||||
]
|
||||
|
||||
@@ -125,6 +121,19 @@ def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
*/
|
||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
|
||||
task downloadDependencies() {
|
||||
description 'Download all dependencies to the Gradle cache'
|
||||
doLast {
|
||||
configurations.findAll().each { config ->
|
||||
if (config.name.contains("minReactNative") && config.canBeResolved) {
|
||||
print config.name
|
||||
print '\n'
|
||||
config.files
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
@@ -134,15 +143,13 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.camerarollexample"
|
||||
applicationId "com.reactnativecommunity.cameraroll.example"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
// Added for detox from https://github.com/wix/Detox/blob/16.5.0/docs/Introduction.Android.md
|
||||
testBuildType System.getProperty('testBuildType', 'debug') // This will later be used to control the test apk build type
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
@@ -159,20 +166,25 @@ android {
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
release {
|
||||
if (project.hasProperty('EXAMPLE_APP_STORE_FILE')) {
|
||||
storeFile file(EXAMPLE_APP_STORE_FILE)
|
||||
storePassword EXAMPLE_APP_STORE_PASSWORD
|
||||
keyAlias EXAMPLE_APP_KEY_ALIAS
|
||||
keyPassword EXAMPLE_APP_KEY_PASSWORD
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
@@ -184,30 +196,17 @@ android {
|
||||
output.versionCodeOverride =
|
||||
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
//noinspection GradleDynamicVersion
|
||||
// You will not need to manually link like this. This is a quirk of the example project
|
||||
implementation project(':react-native-cameraroll')
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
}
|
||||
|
||||
if (enableHermes) {
|
||||
def hermesPath = "../../../node_modules/hermes-engine/android/";
|
||||
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||
@@ -216,11 +215,7 @@ dependencies {
|
||||
implementation jscFlavor
|
||||
}
|
||||
|
||||
// Added for this project (react-native-cameraroll)
|
||||
implementation project(':react-native-cameraroll')
|
||||
|
||||
// Added for detox from https://github.com/wix/Detox/blob/16.5.0/docs/Introduction.Android.md
|
||||
androidTestImplementation('com.wix:detox:+')
|
||||
androidTestImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
// Run this once to be able to run the application with BUCK
|
||||
@@ -230,18 +225,4 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
|
||||
// Added for react-native-circleci-orb from https://github.com/react-native-community/react-native-circleci-orb/tree/v4.4.2#android
|
||||
task downloadDependencies() {
|
||||
description 'Download all dependencies to the Gradle cache'
|
||||
doLast {
|
||||
configurations.findAll().each { config ->
|
||||
if (config.name.contains("minReactNative") && config.canBeResolved) {
|
||||
print config.name
|
||||
print '\n'
|
||||
config.files
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
@@ -12,7 +12,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:usesCleartextTraffic="true"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@@ -2,31 +2,34 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "29.0.2"
|
||||
// detox requires minSdkVersion 18
|
||||
minSdkVersion = 18
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
// Added for detox from https://github.com/wix/Detox/blob/16.5.0/docs/Introduction.Android.md
|
||||
kotlinVersion = "1.3.70"
|
||||
buildToolsVersion = "30.0.2"
|
||||
ndkVersion = "21.4.7075529"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
androidXVersion = "1.+" // Default AndroidX dependency
|
||||
androidXCore = "1.0.2"
|
||||
|
||||
// e2e
|
||||
kotlinVersion = '1.5.10'
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:3.5.3")
|
||||
classpath("com.android.tools.build:gradle:3.4.2")
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
// Added for detox from https://github.com/wix/Detox/blob/16.5.0/docs/Introduction.Android.md
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url("$rootDir/../../node_modules/react-native/android")
|
||||
@@ -37,13 +40,6 @@ allprojects {
|
||||
}
|
||||
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
|
||||
// Start added for detox from https://github.com/wix/Detox/blob/16.5.0/docs/Introduction.Android.md
|
||||
maven {
|
||||
url("$rootDir/../../node_modules/detox/Detox-android")
|
||||
}
|
||||
// End added
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
@@ -25,4 +25,4 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.54.0
|
||||
FLIPPER_VERSION=0.125.0
|
||||
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -14,7 +14,7 @@ const React = require('react');
|
||||
const ReactNative = require('react-native');
|
||||
const {Image, StyleSheet, View, ScrollView} = ReactNative;
|
||||
|
||||
import type {PhotoIdentifier} from '../../js/CameraRoll';
|
||||
import type {PhotoIdentifier} from '../../src/CameraRoll';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
asset: PhotoIdentifier,
|
||||
|
||||
@@ -21,8 +21,8 @@ const {
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} = ReactNative;
|
||||
import CameraRoll from '../../js/CameraRoll';
|
||||
import type {PhotoIdentifier, GroupTypes} from '../../js/CameraRoll';
|
||||
import CameraRoll from '../../src/CameraRoll';
|
||||
import type {PhotoIdentifier, GroupTypes} from '../../src/CameraRoll';
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const {
|
||||
Linking,
|
||||
} = ReactNative;
|
||||
|
||||
import CameraRoll from '../../js/CameraRoll';
|
||||
import {CameraRoll} from '../../src';
|
||||
|
||||
const groupByEveryN = function groupByEveryN(num) {
|
||||
const n = num;
|
||||
@@ -133,7 +133,6 @@ class CameraRollView extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
console.log({data: this.state.data});
|
||||
return (
|
||||
<FlatList
|
||||
keyExtractor={(_, idx) => String(idx)}
|
||||
@@ -166,7 +165,6 @@ class CameraRollView extends React.Component {
|
||||
};
|
||||
|
||||
_renderItem = ({item}) => {
|
||||
console.log({item});
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
{item.map(image => (image ? this.props.renderImage(image) : null))}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import {Component} from 'react';
|
||||
import {
|
||||
SafeAreaView,
|
||||
StyleSheet,
|
||||
@@ -40,7 +41,7 @@ const examples: Example[] = [
|
||||
* Shows a button which opens up a Modal to switch between examples, as well
|
||||
* as the current example itself.
|
||||
*/
|
||||
export default class ExamplesContainer extends React.Component<Props, State> {
|
||||
export default class ExamplesContainer extends Component<Props, State> {
|
||||
state: State = {showChangeExampleModal: false, currentExampleIndex: 0};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Keyboard,
|
||||
} from 'react-native';
|
||||
// @ts-ignore: CameraRollExample has no typings in same folder
|
||||
import CameraRoll from '../../js/CameraRoll';
|
||||
import CameraRoll from '../../src/CameraRoll';
|
||||
|
||||
interface State {
|
||||
fetchingPhotos: boolean;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
143879351AAD238D00F088A5 /* RNCCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RNCCameraRollManager.m */; };
|
||||
2F2D96EC28ABF12100B2EF6B /* RNCCameraRollPermissionModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 2F2D96EB28ABF12100B2EF6B /* RNCCameraRollPermissionModule.m */; };
|
||||
4F788CB822226740001DB9D2 /* RNCAssetsLibraryRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F788CB722226740001DB9D2 /* RNCAssetsLibraryRequestHandler.m */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -26,6 +27,8 @@
|
||||
/* Begin PBXFileReference section */
|
||||
143879331AAD238D00F088A5 /* RNCCameraRollManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RNCCameraRollManager.h; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
|
||||
143879341AAD238D00F088A5 /* RNCCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCCameraRollManager.m; sourceTree = "<group>"; };
|
||||
2F2D96EA28ABF0FE00B2EF6B /* RNCCameraRollPermissionModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNCCameraRollPermissionModule.h; sourceTree = "<group>"; };
|
||||
2F2D96EB28ABF12100B2EF6B /* RNCCameraRollPermissionModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNCCameraRollPermissionModule.m; sourceTree = "<group>"; };
|
||||
4F788CB622226740001DB9D2 /* RNCAssetsLibraryRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNCAssetsLibraryRequestHandler.h; sourceTree = "<group>"; };
|
||||
4F788CB722226740001DB9D2 /* RNCAssetsLibraryRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCAssetsLibraryRequestHandler.m; sourceTree = "<group>"; };
|
||||
58B5115D1A9E6B3D00147676 /* libRNCCameraRoll.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNCCameraRoll.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -45,6 +48,8 @@
|
||||
58B511541A9E6B3D00147676 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2F2D96EB28ABF12100B2EF6B /* RNCCameraRollPermissionModule.m */,
|
||||
2F2D96EA28ABF0FE00B2EF6B /* RNCCameraRollPermissionModule.h */,
|
||||
4F788CB622226740001DB9D2 /* RNCAssetsLibraryRequestHandler.h */,
|
||||
4F788CB722226740001DB9D2 /* RNCAssetsLibraryRequestHandler.m */,
|
||||
143879331AAD238D00F088A5 /* RNCCameraRollManager.h */,
|
||||
@@ -98,11 +103,12 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RNCCameraRoll" */;
|
||||
buildConfigurationList = 58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RNCCameraroll" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
);
|
||||
mainGroup = 58B511541A9E6B3D00147676;
|
||||
@@ -122,6 +128,7 @@
|
||||
files = (
|
||||
4F788CB822226740001DB9D2 /* RNCAssetsLibraryRequestHandler.m in Sources */,
|
||||
143879351AAD238D00F088A5 /* RNCCameraRollManager.m in Sources */,
|
||||
2F2D96EC28ABF12100B2EF6B /* RNCCameraRollPermissionModule.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -266,7 +273,7 @@
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RNCCameraRoll" */ = {
|
||||
58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RNCCameraroll" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
58B5116F1A9E6B3D00147676 /* Debug */,
|
||||
|
||||
@@ -472,6 +472,166 @@ RCT_EXPORT_METHOD(deletePhotos:(NSArray<NSString *>*)assets
|
||||
];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(getPhotoByInternalID:(NSString *)internalId
|
||||
options:(NSDictionary *)options
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
checkPhotoLibraryConfig();
|
||||
|
||||
BOOL const convertHeic = [RCTConvert BOOL:options[@"convertHeicImages"]];
|
||||
|
||||
requestPhotoLibraryAccess(reject, ^(bool isLimited){
|
||||
|
||||
PHFetchResult<PHAsset *> *fetchResult;
|
||||
PHAsset *asset;
|
||||
|
||||
NSString *mediaIdentifier = internalId;
|
||||
|
||||
if ([internalId rangeOfString:@"ph://"].location != NSNotFound) {
|
||||
mediaIdentifier = [internalId stringByReplacingOccurrencesOfString:@"ph://"
|
||||
withString:@""];
|
||||
}
|
||||
|
||||
fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[mediaIdentifier] options:nil];
|
||||
if(fetchResult){
|
||||
asset = fetchResult.firstObject;//only object in the array.
|
||||
}
|
||||
|
||||
if(asset){
|
||||
__block NSURL *imageURL = [[NSURL alloc]initWithString:@""];
|
||||
|
||||
NSString *const assetMediaTypeLabel = (asset.mediaType == PHAssetMediaTypeVideo
|
||||
? @"video"
|
||||
: (asset.mediaType == PHAssetMediaTypeImage
|
||||
? @"image"
|
||||
: (asset.mediaType == PHAssetMediaTypeAudio
|
||||
? @"audio"
|
||||
: @"unknown")));
|
||||
|
||||
|
||||
CLLocation *const loc = asset.location;
|
||||
|
||||
NSArray<PHAssetResource *> *const assetResources = [PHAssetResource assetResourcesForAsset:asset];
|
||||
if (![assetResources firstObject]) {
|
||||
return;
|
||||
}
|
||||
PHAssetResource *const _Nonnull resource = [assetResources firstObject];
|
||||
|
||||
__block NSString *originalFilename = resource.originalFilename;
|
||||
NSString *const uniformMimeType = resource.uniformTypeIdentifier;
|
||||
|
||||
__block NSString *filePath = @"";
|
||||
|
||||
// check if HEIC extension asset
|
||||
if (convertHeic && asset.mediaType == PHAssetMediaTypeImage && [uniformMimeType isEqual: @"public.heic"]) {
|
||||
// convert to JPEG
|
||||
PHImageRequestOptions *const requestOptions = [PHImageRequestOptions new];
|
||||
requestOptions.networkAccessAllowed = YES;
|
||||
requestOptions.version = PHImageRequestOptionsVersionUnadjusted;
|
||||
requestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat;
|
||||
|
||||
CGSize const targetSize = CGSizeMake((CGFloat)asset.pixelWidth, (CGFloat)asset.pixelHeight);
|
||||
[[PHImageManager defaultManager] requestImageForAsset:asset
|
||||
targetSize:targetSize
|
||||
contentMode:PHImageContentModeDefault
|
||||
options:requestOptions
|
||||
resultHandler:^(UIImage * _Nullable image,
|
||||
NSDictionary * _Nullable info) {
|
||||
NSError *const error = [info objectForKey:PHImageErrorKey];
|
||||
if (error) {
|
||||
reject(@"Error while converting to JPEG image",@"Error while converting",error);
|
||||
}
|
||||
|
||||
originalFilename = [originalFilename stringByReplacingOccurrencesOfString:@"HEIC" withString:@"JPEG" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [originalFilename length])];
|
||||
NSData *const imageData = UIImageJPEGRepresentation(image, 1.0);
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSString *fullPath = [NSTemporaryDirectory() stringByAppendingPathComponent:originalFilename];
|
||||
if ([fileManager createFileAtPath:fullPath contents:imageData attributes:nil]) {
|
||||
unsigned long long fileSize = [[fileManager attributesOfItemAtPath:fullPath error:nil] fileSize];
|
||||
|
||||
resolve(@{
|
||||
@"node": @{
|
||||
@"type": assetMediaTypeLabel,
|
||||
@"image": @{
|
||||
@"filepath": fullPath,
|
||||
@"filename": originalFilename,
|
||||
@"height": @([asset pixelHeight]),
|
||||
@"width": @([asset pixelWidth]),
|
||||
@"isStored": @YES,
|
||||
@"playableDuration": @([asset duration]), // fractional seconds
|
||||
@"fileSize": @(fileSize)
|
||||
},
|
||||
@"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
|
||||
} : @{})
|
||||
}
|
||||
});
|
||||
} else {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Failed to create tmp file for asset %@.", originalFilename];
|
||||
NSError *error = RCTErrorWithMessage(errorMessage);
|
||||
reject(@"Error while creating image tmp file",@"Error creating tmp file",error);
|
||||
}
|
||||
|
||||
}];
|
||||
} else {
|
||||
NSNumber* fileSize = [resource valueForKey:@"fileSize"];
|
||||
PHContentEditingInputRequestOptions *const editOptions = [PHContentEditingInputRequestOptions new];
|
||||
// Download asset if on icloud.
|
||||
editOptions.networkAccessAllowed = YES;
|
||||
|
||||
[asset requestContentEditingInputWithOptions:editOptions completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
|
||||
imageURL = contentEditingInput.fullSizeImageURL;
|
||||
if (imageURL.absoluteString.length != 0) {
|
||||
|
||||
filePath = [imageURL.absoluteString stringByReplacingOccurrencesOfString:@"pathfile:" withString:@"file:"];
|
||||
|
||||
resolve(@{
|
||||
@"node": @{
|
||||
@"type": assetMediaTypeLabel,
|
||||
@"image": @{
|
||||
@"filepath": filePath,
|
||||
@"filename": originalFilename,
|
||||
@"height": @([asset pixelHeight]),
|
||||
@"width": @([asset pixelWidth]),
|
||||
@"isStored": @YES,
|
||||
@"playableDuration": @([asset duration]), // fractional seconds
|
||||
@"fileSize": fileSize
|
||||
},
|
||||
@"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
|
||||
} : @{})
|
||||
}
|
||||
});
|
||||
} else {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset"
|
||||
" with localIdentifier %@ with no error message.", internalId];
|
||||
NSError *error = RCTErrorWithMessage(errorMessage);
|
||||
reject(@"Error while getting file path",@"Error while getting file path",error);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
} else {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset"
|
||||
" with localIdentifier %@ with no error message.", internalId];
|
||||
NSError *error = RCTErrorWithMessage(errorMessage);
|
||||
reject(@"No asset found",@"No asset found",error);
|
||||
}
|
||||
|
||||
}, false);
|
||||
}
|
||||
|
||||
static void checkPhotoLibraryConfig()
|
||||
{
|
||||
#if RCT_DEV
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// CameraRollPermissionModule.h
|
||||
// RNCCameraRoll
|
||||
//
|
||||
// Created by sakhi idris on 16/08/2022.
|
||||
// Copyright © 2022 Facebook. All rights reserved.
|
||||
//
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
typedef enum {
|
||||
RNPermissionStatusNotDetermined = 0,
|
||||
RNPermissionStatusRestricted = 1,
|
||||
RNPermissionStatusDenied = 2,
|
||||
RNPermissionStatusAuthorized = 3,
|
||||
RNPermissionStatusLimited = 4,
|
||||
} RNPermissionStatus;
|
||||
|
||||
@interface RNCCameraRollPermissionModule : RCTEventEmitter <RCTBridgeModule, PHPhotoLibraryChangeObserver>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,192 @@
|
||||
//
|
||||
// CameraRollPermissionModule.m
|
||||
// RNCCameraRoll
|
||||
//
|
||||
// Created by sakhi idris on 16/08/2022.
|
||||
// Copyright © 2022 Facebook. All rights reserved.
|
||||
//
|
||||
#import "RNCCameraRollPermissionModule.h"
|
||||
#import <React/RCTUtils.h>
|
||||
#import <React/RCTConvert.h>
|
||||
|
||||
@import Photos;
|
||||
@import PhotosUI;
|
||||
|
||||
@implementation RNCCameraRollPermissionModule
|
||||
|
||||
{
|
||||
bool hasListeners;
|
||||
}
|
||||
|
||||
#pragma mark - Access Levels
|
||||
static NSString * const ADD_ONLY = @"addOnly";
|
||||
static NSString * const READ_WRITE = @"readWrite";
|
||||
|
||||
// Will be called when this module's first listener is added.
|
||||
-(void)startObserving {
|
||||
hasListeners = YES;
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
|
||||
}
|
||||
|
||||
// Will be called when this module's last listener is removed, or on dealloc.
|
||||
-(void)stopObserving {
|
||||
hasListeners = NO;
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (dispatch_queue_t)methodQueue
|
||||
{
|
||||
return dispatch_get_main_queue();
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents {
|
||||
return @[@"onLibrarySelectionChange"];
|
||||
}
|
||||
|
||||
- (NSString *)stringForStatus:(RNPermissionStatus)status {
|
||||
switch (status) {
|
||||
case RNPermissionStatusRestricted:
|
||||
return @"unavailable";
|
||||
case RNPermissionStatusNotDetermined:
|
||||
return @"not-determined";
|
||||
case RNPermissionStatusDenied:
|
||||
return @"denied";
|
||||
case RNPermissionStatusLimited:
|
||||
return @"limited";
|
||||
case RNPermissionStatusAuthorized:
|
||||
return @"granted";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (void)checkCameraRollPermission:(NSString *) accessLevel
|
||||
resolver:(void (^ _Nonnull)(RNPermissionStatus))resolve
|
||||
rejecter:(void (^ _Nonnull)(NSString *code, NSString *message))reject {
|
||||
PHAuthorizationStatus status;
|
||||
|
||||
if (@available(iOS 14.0, *)) {
|
||||
PHAccessLevel requestedAccessLevel;
|
||||
if ([accessLevel isEqualToString: ADD_ONLY]) {
|
||||
requestedAccessLevel = PHAccessLevelAddOnly;
|
||||
} else if ([accessLevel isEqualToString: READ_WRITE]) {
|
||||
requestedAccessLevel = PHAccessLevelReadWrite;
|
||||
} else {
|
||||
return reject(@"incorrect_access_level", @"The requested access level does not exist");
|
||||
}
|
||||
status = [PHPhotoLibrary authorizationStatusForAccessLevel:requestedAccessLevel];
|
||||
} else {
|
||||
status = [PHPhotoLibrary authorizationStatus];
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case PHAuthorizationStatusNotDetermined:
|
||||
return resolve(RNPermissionStatusNotDetermined);
|
||||
case PHAuthorizationStatusRestricted:
|
||||
return resolve(RNPermissionStatusRestricted);
|
||||
case PHAuthorizationStatusDenied:
|
||||
return resolve(RNPermissionStatusDenied);
|
||||
case PHAuthorizationStatusLimited:
|
||||
return resolve(RNPermissionStatusLimited);
|
||||
case PHAuthorizationStatusAuthorized:
|
||||
return resolve(RNPermissionStatusAuthorized);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)requestCameraRollReadWritePermission:(void (^ _Nonnull)(RNPermissionStatus))resolve
|
||||
rejecter:(void (^ _Nonnull)(NSString *code, NSString *message))reject {
|
||||
if (@available(iOS 14.0, *)) {
|
||||
[PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelReadWrite handler:^(__unused PHAuthorizationStatus status) {
|
||||
[self checkCameraRollPermission: READ_WRITE resolver: resolve rejecter:reject];
|
||||
}];
|
||||
} else {
|
||||
[PHPhotoLibrary requestAuthorization:^(__unused PHAuthorizationStatus status) {
|
||||
[self checkCameraRollPermission: READ_WRITE resolver: resolve rejecter:reject];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)requestCameraRollAddOnlyPermission:(void (^ _Nonnull)(RNPermissionStatus))resolve
|
||||
rejecter:(void (^ _Nonnull)(NSString *code, NSString *message))reject {
|
||||
if (@available(iOS 14.0, *)) {
|
||||
[PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelAddOnly handler:^(__unused PHAuthorizationStatus status) {
|
||||
[self checkCameraRollPermission: ADD_ONLY resolver: resolve rejecter:reject];
|
||||
}];
|
||||
} else {
|
||||
[PHPhotoLibrary requestAuthorization:^(__unused PHAuthorizationStatus status) {
|
||||
[self checkCameraRollPermission: ADD_ONLY resolver: resolve rejecter:reject];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)refreshLimitedPhotoselection:(RCTPromiseResolveBlock _Nonnull)resolve
|
||||
rejecter:(RCTPromiseRejectBlock _Nonnull)reject {
|
||||
if (@available(iOS 14, *)) {
|
||||
if ([PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite] != PHAuthorizationStatusLimited) {
|
||||
return reject(@"cannot_open_limited_picker", @"Photo library permission isn't limited", nil);
|
||||
}
|
||||
|
||||
UIViewController *presentedViewController = RCTPresentedViewController();
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] presentLimitedLibraryPickerFromViewController:presentedViewController];
|
||||
|
||||
resolve(@(true));
|
||||
} else {
|
||||
reject(@"cannot_open_limited_picker", @"Available on iOS 14 or higher", nil);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)photoLibraryDidChange:(PHChange *)changeInstance
|
||||
{
|
||||
if (hasListeners && changeInstance != nil) {
|
||||
[self sendEventWithName:@"onLibrarySelectionChange" body:@"Changes occured"];
|
||||
}
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(checkPermission:
|
||||
(NSString *) accessLevel
|
||||
resolve: (RCTPromiseResolveBlock)resolve
|
||||
reject: (RCTPromiseRejectBlock)reject) {
|
||||
|
||||
[self checkCameraRollPermission:accessLevel resolver:^(RNPermissionStatus status) {
|
||||
resolve([self stringForStatus:status]);
|
||||
} rejecter:^(NSString *code, NSString *message) {
|
||||
reject(code, message, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
RCT_EXPORT_METHOD(requestReadWritePermission:
|
||||
(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
|
||||
[self requestCameraRollReadWritePermission:^(RNPermissionStatus status) {
|
||||
resolve([self stringForStatus:status]);
|
||||
} rejecter:^(NSString *code, NSString *message) {
|
||||
reject(code, message, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(requestAddOnlyPermission:
|
||||
(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
|
||||
[self requestCameraRollAddOnlyPermission:^(RNPermissionStatus status) {
|
||||
resolve([self stringForStatus:status]);
|
||||
} rejecter:^(NSString *code, NSString *message) {
|
||||
reject(code, message, nil);
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
RCT_REMAP_METHOD(refreshPhotoSelection,
|
||||
refreshLimitedPhotoselectionWithResolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
[self refreshLimitedPhotoselection:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export default require('react-native').NativeModules.RNCCameraRoll;
|
||||
+56
-33
@@ -4,17 +4,30 @@
|
||||
"homepage": "https://github.com/react-native-community/react-native-cameraroll#readme",
|
||||
"version": "4.1.2",
|
||||
"description": "React Native Camera Roll for iOS & Android",
|
||||
"main": "./js/CameraRoll.js",
|
||||
"types": "./typings/CameraRoll.d.ts",
|
||||
"main": "lib/commonjs/index",
|
||||
"module": "lib/module/index",
|
||||
"types": "lib/typescript/index.d.ts",
|
||||
"react-native": "src/index.ts",
|
||||
"source": "src/index",
|
||||
"files": [
|
||||
"src/",
|
||||
"lib/",
|
||||
"android/",
|
||||
"ios/",
|
||||
"react-native-cameraroll.podspec",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "bob build",
|
||||
"start": "react-native start",
|
||||
"start:android": "react-native run-android --root example/",
|
||||
"start:ios": "react-native run-ios --project-path example/ios",
|
||||
"test": "yarn validate:eslint && yarn validate:flow && yarn validate:typescript && yarn test:jest",
|
||||
"validate:eslint": "eslint 'js/**/*.js' 'example/**/*.js'",
|
||||
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
||||
"validate:eslint": "eslint 'src/**/*.ts'",
|
||||
"validate:flow": "flow check",
|
||||
"validate:typescript": "tsc --project ./",
|
||||
"test:jest": "jest js/",
|
||||
"test:jest": "jest src/",
|
||||
"test:detox:android:test:debug": "detox test -c android.emu.debug",
|
||||
"test:detox:android:test:release": "detox test -c android.emu.release",
|
||||
"test:detox:android:build:debug": "detox build -c android.emu.debug",
|
||||
@@ -38,33 +51,47 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "16 || 17 || 18",
|
||||
"react-native": ">=0.60"
|
||||
"react-native": ">=0.59"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"@react-native-community/eslint-config": "^2.0.0",
|
||||
"@semantic-release/git": "7.0.8",
|
||||
"@types/react-native": "^0.62.10",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^26.0.1",
|
||||
"babel-plugin-module-resolver": "^3.2.0",
|
||||
"detox": "^16.5.0",
|
||||
"eslint": "^7.0.0",
|
||||
"eslint-plugin-prettier": "^3.0.1",
|
||||
"flow-bin": "^0.122.0",
|
||||
"husky": "^2.2.0",
|
||||
"jest": "^26.0.1",
|
||||
"metro-react-native-babel-preset": "^0.59.0",
|
||||
"prettier": "^1.17.0",
|
||||
"pretty-quick": "^1.10.0",
|
||||
"react": "16.13.1",
|
||||
"react-native": "0.63.4",
|
||||
"react-test-renderer": "16.11.0",
|
||||
"rimraf": "^2.6.3",
|
||||
"semantic-release": "15.13.3",
|
||||
"typescript": "^3.4.1"
|
||||
"react-native-builder-bob": "0.18.3",
|
||||
"@babel/core": "7.18.10",
|
||||
"@babel/runtime": "7.18.9",
|
||||
"@react-native-community/eslint-config": "3.1.0",
|
||||
"@semantic-release/git": "10.0.1",
|
||||
"@types/react-native": "0.62.10",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"babel-jest": "28.1.3",
|
||||
"detox": "19.9.3",
|
||||
"eslint": "8.22.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"husky": "8.0.1",
|
||||
"jest": "28.1.3",
|
||||
"@types/jest": "28.1.7",
|
||||
"prettier": "2.7.1",
|
||||
"metro-react-native-babel-preset": "0.72.0",
|
||||
"pretty-quick": "3.1.3",
|
||||
"react": "17.0.2",
|
||||
"react-native": "0.66.0",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"semantic-release": "19.0.3",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"react-native-builder-bob": {
|
||||
"source": "src",
|
||||
"output": "lib",
|
||||
"targets": [
|
||||
"commonjs",
|
||||
"module",
|
||||
[
|
||||
"typescript",
|
||||
{
|
||||
"project": "tsconfig.json"
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"rn-docs": {
|
||||
"title": "CameraRoll",
|
||||
@@ -76,7 +103,6 @@
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"lodash": "4.17.15",
|
||||
"@react-native-community/cli-platform-android": "~4.3.0"
|
||||
},
|
||||
"jest": {
|
||||
@@ -126,8 +152,5 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/react-native-community/react-native-cameraroll.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,10 @@
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
import {Platform} from 'react-native';
|
||||
import RNCCameraRoll from './nativeInterface';
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
const GROUP_TYPES_OPTIONS = {
|
||||
Album: 'Album',
|
||||
All: 'All', // default
|
||||
@@ -29,7 +23,14 @@ const ASSET_TYPE_OPTIONS = {
|
||||
Photos: 'Photos',
|
||||
};
|
||||
|
||||
export type GroupTypes = $Keys<typeof GROUP_TYPES_OPTIONS>;
|
||||
export type GroupTypes =
|
||||
| 'Album'
|
||||
| 'All'
|
||||
| 'Event'
|
||||
| 'Faces'
|
||||
| 'Library'
|
||||
| 'PhotoStream'
|
||||
| 'SavedPhotos';
|
||||
|
||||
export type Include =
|
||||
| 'filename'
|
||||
@@ -38,6 +39,8 @@ export type Include =
|
||||
| 'imageSize'
|
||||
| 'playableDuration';
|
||||
|
||||
export type AssetType = 'All' | 'Videos' | 'Photos';
|
||||
|
||||
/**
|
||||
* Shape of the param arg for the `getPhotos` function.
|
||||
*/
|
||||
@@ -46,97 +49,101 @@ export type GetPhotosParams = {
|
||||
* The number of photos wanted in reverse order of the photo application
|
||||
* (i.e. most recent first).
|
||||
*/
|
||||
first: number,
|
||||
first: number;
|
||||
|
||||
/**
|
||||
* A cursor that matches `page_info { end_cursor }` returned from a previous
|
||||
* call to `getPhotos`
|
||||
*/
|
||||
after?: string,
|
||||
after?: string;
|
||||
|
||||
/**
|
||||
* Specifies which group types to filter the results to.
|
||||
*/
|
||||
groupTypes?: GroupTypes,
|
||||
groupTypes?: GroupTypes;
|
||||
|
||||
/**
|
||||
* Specifies filter on group names, like 'Recent Photos' or custom album
|
||||
* titles.
|
||||
*/
|
||||
groupName?: string,
|
||||
groupName?: string;
|
||||
|
||||
/**
|
||||
* Specifies filter on asset type
|
||||
*/
|
||||
assetType?: $Keys<typeof ASSET_TYPE_OPTIONS>,
|
||||
assetType?: AssetType;
|
||||
|
||||
/**
|
||||
* Earliest time to get photos from. A timestamp in milliseconds. Exclusive.
|
||||
*/
|
||||
fromTime?: number,
|
||||
fromTime?: number;
|
||||
|
||||
/**
|
||||
* Latest time to get photos from. A timestamp in milliseconds. Inclusive.
|
||||
*/
|
||||
toTime?: Number,
|
||||
toTime?: number;
|
||||
|
||||
/**
|
||||
* Filter by mimetype (e.g. image/jpeg).
|
||||
*/
|
||||
mimeTypes?: Array<string>,
|
||||
mimeTypes?: Array<string>;
|
||||
|
||||
/**
|
||||
* Specific fields in the output that we want to include, even though they
|
||||
* might have some performance impact.
|
||||
*/
|
||||
include?: Include[],
|
||||
include?: Include[];
|
||||
};
|
||||
|
||||
export type PhotoIdentifier = {
|
||||
node: {
|
||||
type: string,
|
||||
group_name: string,
|
||||
type: string;
|
||||
group_name: string;
|
||||
image: {
|
||||
filename: string | null,
|
||||
uri: string,
|
||||
height: number,
|
||||
width: number,
|
||||
fileSize: number | null,
|
||||
playableDuration: number,
|
||||
},
|
||||
timestamp: number,
|
||||
filename: string | null;
|
||||
uri: string;
|
||||
height: number;
|
||||
width: number;
|
||||
fileSize: number | null;
|
||||
playableDuration: number;
|
||||
};
|
||||
timestamp: number;
|
||||
location: {
|
||||
latitude?: number,
|
||||
longitude?: number,
|
||||
altitude?: number,
|
||||
heading?: number,
|
||||
speed?: number,
|
||||
} | null,
|
||||
},
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
altitude?: number;
|
||||
heading?: number;
|
||||
speed?: number;
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type PhotoConvertionOptions = {
|
||||
convertHeicImages: boolean;
|
||||
};
|
||||
|
||||
export type PhotoIdentifiersPage = {
|
||||
edges: Array<PhotoIdentifier>,
|
||||
edges: Array<PhotoIdentifier>;
|
||||
page_info: {
|
||||
has_next_page: boolean,
|
||||
start_cursor?: string,
|
||||
end_cursor?: string,
|
||||
},
|
||||
limited?: boolean,
|
||||
has_next_page: boolean;
|
||||
start_cursor?: string;
|
||||
end_cursor?: string;
|
||||
};
|
||||
limited?: boolean;
|
||||
};
|
||||
|
||||
export type SaveToCameraRollOptions = {
|
||||
type?: 'photo' | 'video' | 'auto',
|
||||
album?: string,
|
||||
type?: 'photo' | 'video' | 'auto';
|
||||
album?: string;
|
||||
};
|
||||
|
||||
export type GetAlbumsParams = {
|
||||
assetType?: $Keys<typeof ASSET_TYPE_OPTIONS>,
|
||||
assetType?: AssetType;
|
||||
};
|
||||
|
||||
export type Album = {
|
||||
title: string,
|
||||
count: number,
|
||||
title: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -144,26 +151,16 @@ export type Album = {
|
||||
*
|
||||
* See https://facebook.github.io/react-native/docs/cameraroll.html
|
||||
*/
|
||||
class CameraRoll {
|
||||
export class CameraRoll {
|
||||
static GroupTypesOptions = GROUP_TYPES_OPTIONS;
|
||||
static AssetTypeOptions = ASSET_TYPE_OPTIONS;
|
||||
|
||||
/**
|
||||
* `CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.
|
||||
*/
|
||||
static saveImageWithTag(tag: string): Promise<string> {
|
||||
console.warn(
|
||||
'`CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.',
|
||||
);
|
||||
return this.saveToCameraRoll(tag, 'photo');
|
||||
}
|
||||
|
||||
/**
|
||||
* On iOS: requests deletion of a set of photos from the camera roll.
|
||||
* On Android: Deletes a set of photos from the camera roll.
|
||||
*
|
||||
*/
|
||||
static deletePhotos(photoUris: Array<string>) {
|
||||
static deletePhotos(photoUris: Array<string>): void {
|
||||
return RNCCameraRoll.deletePhotos(photoUris);
|
||||
}
|
||||
|
||||
@@ -175,28 +172,18 @@ class CameraRoll {
|
||||
tag: string,
|
||||
options: SaveToCameraRollOptions = {},
|
||||
): Promise<string> {
|
||||
let {type = 'auto', album = ''} = options;
|
||||
invariant(
|
||||
typeof tag === 'string',
|
||||
'CameraRoll.saveToCameraRoll must be a valid string.',
|
||||
);
|
||||
invariant(
|
||||
options.type === 'photo' ||
|
||||
options.type === 'video' ||
|
||||
options.type === 'auto' ||
|
||||
options.type === undefined,
|
||||
`The second argument to saveToCameraRoll must be 'photo' or 'video' or 'auto'. You passed ${type ||
|
||||
'unknown'}`,
|
||||
);
|
||||
let {type = 'auto'} = options;
|
||||
const {album = ''} = options;
|
||||
if (tag === '') throw new Error('tag must be a valid string');
|
||||
|
||||
if (type === 'auto') {
|
||||
if (['mov', 'mp4'].indexOf(tag.split('.').slice(-1)[0]) >= 0) {
|
||||
type = 'video';
|
||||
} else {
|
||||
type = 'photo';
|
||||
}
|
||||
const fileExtension = tag.split('.').slice(-1)[0] ?? '';
|
||||
if (['mov', 'mp4'].indexOf(fileExtension) >= 0) type = 'video';
|
||||
else type = 'photo';
|
||||
}
|
||||
return RNCCameraRoll.saveToCameraRoll(tag, {type, album});
|
||||
}
|
||||
|
||||
static saveToCameraRoll(
|
||||
tag: string,
|
||||
type?: 'photo' | 'video' | 'auto',
|
||||
@@ -207,19 +194,18 @@ class CameraRoll {
|
||||
return CameraRoll.save(tag, {type});
|
||||
}
|
||||
static getAlbums(
|
||||
params?: GetAlbumsParams = {assetType: ASSET_TYPE_OPTIONS.All},
|
||||
params: GetAlbumsParams = {assetType: 'All'},
|
||||
): Promise<Album[]> {
|
||||
return RNCCameraRoll.getAlbums(params);
|
||||
}
|
||||
|
||||
static getParamsWithDefaults(params: GetPhotosParams): GetPhotosParams {
|
||||
const newParams = {...params};
|
||||
if (!newParams.assetType) {
|
||||
newParams.assetType = ASSET_TYPE_OPTIONS.All;
|
||||
}
|
||||
if (!newParams.groupTypes && Platform.OS !== 'android') {
|
||||
newParams.groupTypes = GROUP_TYPES_OPTIONS.All;
|
||||
}
|
||||
if (newParams.assetType === undefined) newParams.assetType = 'All';
|
||||
|
||||
if (newParams.groupTypes === undefined && Platform.OS !== 'android')
|
||||
newParams.groupTypes = 'All';
|
||||
|
||||
return newParams;
|
||||
}
|
||||
|
||||
@@ -230,20 +216,25 @@ class CameraRoll {
|
||||
* See https://facebook.github.io/react-native/docs/cameraroll.html#getphotos
|
||||
*/
|
||||
static getPhotos(params: GetPhotosParams): Promise<PhotoIdentifiersPage> {
|
||||
params = CameraRoll.getParamsWithDefaults(params);
|
||||
const promise = RNCCameraRoll.getPhotos(params);
|
||||
params = this.getParamsWithDefaults(params);
|
||||
return RNCCameraRoll.getPhotos(params);
|
||||
}
|
||||
|
||||
if (arguments.length > 1) {
|
||||
console.warn(
|
||||
'CameraRoll.getPhotos(tag, success, error) is deprecated. Use the returned Promise instead',
|
||||
);
|
||||
let successCallback = arguments[1];
|
||||
const errorCallback = arguments[2] || (() => {});
|
||||
promise.then(successCallback, errorCallback);
|
||||
}
|
||||
|
||||
return promise;
|
||||
/**
|
||||
* Returns a Promise with photo internal path.
|
||||
* if conversion is requested from HEIC then temporary file is created.
|
||||
*
|
||||
* @param internalID - PH photo internal ID.
|
||||
* @param convertHeicImages - whether to convert or not heic images to JPEG.
|
||||
* @returns Promise<PhotoIdentifier>
|
||||
*/
|
||||
static iosGetImageDataById(
|
||||
internalID: string,
|
||||
convertHeicImages = false,
|
||||
): Promise<PhotoIdentifier> {
|
||||
const conversionOption: PhotoConvertionOptions = {
|
||||
convertHeicImages: convertHeicImages,
|
||||
};
|
||||
return RNCCameraRoll.getPhotoByInternalID(internalID, conversionOption);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CameraRoll;
|
||||
@@ -0,0 +1,51 @@
|
||||
import {NativeEventEmitter, Platform} from 'react-native';
|
||||
import CameraRollPermissionModule from './cameraRollPermissionNativeInterface';
|
||||
|
||||
/** Defines ios permission access levels for gallery */
|
||||
export type AccessLevel = 'addOnly' | 'readWrite';
|
||||
|
||||
export type CameraRollAuthorizationStatus =
|
||||
| 'granted'
|
||||
| 'limited'
|
||||
| 'denied'
|
||||
| 'unavailable'
|
||||
| 'blocked'
|
||||
| 'not-determined';
|
||||
|
||||
const isIOS = Platform.OS === 'ios';
|
||||
if (isIOS && CameraRollPermissionModule == null) {
|
||||
console.error(
|
||||
"photoLibraryPermissionModule: Native Module 'photoLibraryPermissionModule' was null! Did you run pod install?",
|
||||
);
|
||||
}
|
||||
export const cameraRollEventEmitter = new NativeEventEmitter(
|
||||
isIOS ? CameraRollPermissionModule : undefined,
|
||||
);
|
||||
|
||||
export const iosReadGalleryPermission = (
|
||||
accessLevel: AccessLevel,
|
||||
): Promise<CameraRollAuthorizationStatus> => {
|
||||
if (!isIOS) throw new Error('this module is available only for ios');
|
||||
|
||||
return CameraRollPermissionModule.checkPermission(accessLevel);
|
||||
};
|
||||
|
||||
export const iosRequestReadWriteGalleryPermission =
|
||||
(): Promise<CameraRollAuthorizationStatus> => {
|
||||
if (!isIOS) throw new Error('this module is available only for ios');
|
||||
|
||||
return CameraRollPermissionModule.requestReadWritePermission();
|
||||
};
|
||||
|
||||
export const iosRequestAddOnlyGalleryPermission =
|
||||
(): Promise<CameraRollAuthorizationStatus> => {
|
||||
if (!isIOS) throw new Error('this module is available only for ios');
|
||||
|
||||
return CameraRollPermissionModule.requestAddOnlyPermission();
|
||||
};
|
||||
|
||||
export const iosRefreshGallerySelection = (): Promise<boolean> => {
|
||||
if (!isIOS) throw new Error('this module is available only for ios');
|
||||
|
||||
return CameraRollPermissionModule.refreshPhotoSelection();
|
||||
};
|
||||
@@ -1,16 +1,6 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
import {CameraRoll} from '../CameraRoll';
|
||||
|
||||
import CameraRoll from '../CameraRoll';
|
||||
|
||||
const NativeModule = require('../nativeInterface');
|
||||
import NativeModule from '../nativeInterface';
|
||||
|
||||
jest.mock('../nativeInterface');
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import {NativeModules} from 'react-native';
|
||||
|
||||
export default NativeModules.RNCCameraRollPermissionModule;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './CameraRoll';
|
||||
export * from './CameraRollIOSPermission';
|
||||
@@ -0,0 +1,3 @@
|
||||
import {NativeModules} from 'react-native';
|
||||
|
||||
export default NativeModules.RNCCameraRoll;
|
||||
+20
-18
@@ -1,25 +1,27 @@
|
||||
{
|
||||
"include": ["typings/**/*.d.ts", "example/**/*.ts", "example/**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"importHelpers": true,
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"allowJs": false,
|
||||
"allowUnreachableCode": false,
|
||||
"allowUnusedLabels": false,
|
||||
"esModuleInterop": true,
|
||||
"importsNotUsedAsValues": "error",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"jsx": "react-native",
|
||||
"lib": ["esnext"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noStrictGenericChecks": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"lib": ["es2015", "es2016", "esnext", "dom"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true
|
||||
"strict": true,
|
||||
"target": "esnext",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
"include": ["src", ".eslintrc.js", "babel.config.js"],
|
||||
"exclude": ["node_modules", "lib", "docs", "example"]
|
||||
}
|
||||
|
||||
Vendored
-182
@@ -1,182 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
declare namespace CameraRoll {
|
||||
type GroupType =
|
||||
| 'Album'
|
||||
| 'All'
|
||||
| 'Event'
|
||||
| 'Faces'
|
||||
| 'Library'
|
||||
| 'PhotoStream'
|
||||
| 'SavedPhotos';
|
||||
|
||||
type AssetType = 'All' | 'Videos' | 'Photos';
|
||||
|
||||
type Include =
|
||||
/** Ensures the filename is included. Has a large performance hit on iOS */
|
||||
| 'filename'
|
||||
/** Ensures the fileSize is included. Has a large performance hit on iOS */
|
||||
| 'fileSize'
|
||||
/** Ensures the location is included. Has a medium performance hit on Android */
|
||||
| 'location'
|
||||
/** Ensures the image width and height are included. Has a small performance hit on Android */
|
||||
| 'imageSize'
|
||||
/** Ensures the image playableDuration is included. Has a medium performance hit on Android */
|
||||
| 'playableDuration';
|
||||
|
||||
/**
|
||||
* Shape of the param arg for the `getPhotosFast` function.
|
||||
*/
|
||||
interface GetPhotosParams {
|
||||
/**
|
||||
* The number of photos wanted in reverse order of the photo application
|
||||
* (i.e. most recent first).
|
||||
*/
|
||||
first: number;
|
||||
|
||||
/**
|
||||
* A cursor that matches `page_info { end_cursor }` returned from a previous
|
||||
* call to `getPhotos`. Note that using this will reduce performance
|
||||
* slightly on iOS. An alternative is just using the `fromTime` and `toTime`
|
||||
* filters, which have no such impact.
|
||||
*/
|
||||
after?: string;
|
||||
|
||||
/**
|
||||
* Specifies which group types to filter the results to.
|
||||
*/
|
||||
groupTypes?: GroupType;
|
||||
|
||||
/**
|
||||
* Specifies filter on group names, like 'Recent Photos' or custom album
|
||||
* titles.
|
||||
*/
|
||||
groupName?: string;
|
||||
|
||||
/**
|
||||
* Specifies filter on asset type
|
||||
*/
|
||||
assetType?: AssetType;
|
||||
|
||||
/**
|
||||
* Filter by creation time with a timestamp in milliseconds. This time is
|
||||
* exclusive, so we'll select all photos with `timestamp > fromTime`.
|
||||
*/
|
||||
fromTime?: number;
|
||||
|
||||
/**
|
||||
* Filter by creation time with a timestamp in milliseconds. This time is
|
||||
* inclusive, so we'll select all photos with `timestamp <= toTime`.
|
||||
*/
|
||||
toTime?: number;
|
||||
|
||||
/**
|
||||
* Filter by mimetype (e.g. image/jpeg). Note that using this will reduce
|
||||
* performance slightly on iOS.
|
||||
*/
|
||||
mimeTypes?: Array<string>;
|
||||
|
||||
/**
|
||||
* Specific fields in the output that we want to include, even though they
|
||||
* might have some performance impact.
|
||||
*/
|
||||
include?: Include[];
|
||||
}
|
||||
|
||||
interface PhotoIdentifier {
|
||||
node: {
|
||||
type: string;
|
||||
group_name: string;
|
||||
image: {
|
||||
/** Only set if the `include` parameter contains `filename`. */
|
||||
filename: string | null;
|
||||
uri: string;
|
||||
/** Only set if the `include` parameter contains `imageSize`. */
|
||||
height: number;
|
||||
/** Only set if the `include` parameter contains `imageSize`. */
|
||||
width: number;
|
||||
/** Only set if the `include` parameter contains `fileSize`. */
|
||||
fileSize: number | null;
|
||||
/**
|
||||
* Only set if the `include` parameter contains `playableDuration`.
|
||||
* Will be null for images.
|
||||
*/
|
||||
playableDuration: number | null;
|
||||
};
|
||||
/** Timestamp in seconds. */
|
||||
timestamp: number;
|
||||
/** Only set if the `include` parameter contains `location`. */
|
||||
location: {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
altitude?: number;
|
||||
heading?: number;
|
||||
speed?: number;
|
||||
} | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface PhotoIdentifiersPage {
|
||||
edges: Array<PhotoIdentifier>;
|
||||
page_info: {
|
||||
has_next_page: boolean;
|
||||
start_cursor?: string;
|
||||
end_cursor?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAlbumsParams {
|
||||
assetType?: AssetType;
|
||||
}
|
||||
|
||||
interface Album {
|
||||
title: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
type SaveToCameraRollOptions = {
|
||||
type?: 'photo' | 'video' | 'auto';
|
||||
album?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* `CameraRoll.saveImageWithTag()` is deprecated. Use `CameraRoll.saveToCameraRoll()` instead.
|
||||
*/
|
||||
function saveImageWithTag(tag: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Delete a photo from the camera roll or media library. photoUris is an array of photo uri's.
|
||||
*/
|
||||
function deletePhotos(photoUris: Array<string>): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Saves the photo or video to the camera roll or photo library.
|
||||
*/
|
||||
function saveToCameraRoll(
|
||||
tag: string,
|
||||
type?: 'photo' | 'video',
|
||||
): Promise<string>;
|
||||
|
||||
/**
|
||||
* Saves the photo or video to the camera roll or photo library.
|
||||
*/
|
||||
function save(
|
||||
tag: string,
|
||||
options?: SaveToCameraRollOptions,
|
||||
): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns a Promise with photo identifier objects from the local camera
|
||||
* roll of the device matching shape defined by `getPhotosReturnChecker`.
|
||||
*/
|
||||
function getPhotos(params: GetPhotosParams): Promise<PhotoIdentifiersPage>;
|
||||
|
||||
function getAlbums(params: GetAlbumsParams): Promise<Album[]>;
|
||||
}
|
||||
|
||||
export = CameraRoll;
|
||||
Reference in New Issue
Block a user