diff --git a/README.md b/README.md index 708c7d16f..c63305a1c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# react-native-image-picker 🎆 +# @exodus/react-native-image-picker 🎆 A React Native module that allows you to select a photo/video from the device library or camera. @@ -9,7 +9,7 @@ A React Native module that allows you to select a photo/video from the device li ## Installation ```bash -yarn add react-native-image-picker +yarn add @exodus/react-native-image-picker ``` ### New Architecture @@ -71,7 +71,10 @@ For more details, consult the Android documentation on AndroidX Photo Picker: [h ## Methods ```js -import {launchCamera, launchImageLibrary} from 'react-native-image-picker'; +import { + launchCamera, + launchImageLibrary, +} from '@exodus/react-native-image-picker'; ``` ### `launchCamera()` @@ -106,59 +109,57 @@ The `callback` will be called with a response object, refer to [The Response Obj ## Options -| Option | iOS | Android | Web | Description | -| ----------------------- | --- | ------- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| mediaType | OK | OK | OK | `photo` or `video` or `mixed`(`launchCamera` on Android does not support 'mixed'). Web only supports 'photo' for now. | -| restrictMimeTypes | NO | OK | NO | Array containing the mime-types allowed to be picked. Default is empty (everything). | -| maxWidth | OK | OK | NO | To resize the image. | -| maxHeight | OK | OK | NO | To resize the image. | -| videoQuality | OK | OK | NO | `low`, `medium`, or `high` on iOS, `low` or `high` on Android. | -| durationLimit | OK | OK | NO | Video max duration (in seconds). | -| quality | OK | OK | NO | 0 to 1, photos. | -| conversionQuality | NO | OK | NO | For conversion from HEIC/HEIF to JPEG, 0 to 1. Default is `0.92` | -| cameraType | OK | OK | NO | 'back' or 'front' (May not be supported in few android devices). | -| includeBase64 | OK | OK | OK | If `true`, creates base64 string of the image (Avoid using on large image files due to performance). | -| includeExtra | OK | OK | NO | If `true`, will include extra data which requires library permissions to be requested (i.e. exif data). | -| saveToPhotos | OK | OK | NO | (Boolean) Only for `launchCamera`, saves the image/video file captured to public photo. | -| selectionLimit | OK | OK | OK | Supports providing any integer value. Use `0` to allow any number of files on iOS version >= 14 & Android version >= 13. Default is `1`. | -| presentationStyle | OK | NO | NO | Controls how the picker is presented. `currentContext`, `pageSheet`, `fullScreen`, `formSheet`, `popover`, `overFullScreen`, `overCurrentContext`. Default is `currentContext`. | -| formatAsMp4 | OK | NO | NO | Converts the selected video to MP4 (iOS Only). | -| assetRepresentationMode | OK | OK | NO | A mode that determines which representation to use if an asset contains more than one on iOS or disables HEIC/HEIF to JPEG conversion on Android if set to 'current'. Possible values: 'auto', 'current', 'compatible'. Default is 'auto'. | +| Option | iOS | Android | Description | +| ----------------------- | --- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| mediaType | OK | OK | `photo` or `video` or `mixed`(`launchCamera` on Android does not support 'mixed'). | +| restrictMimeTypes | NO | OK | Array containing the mime-types allowed to be picked. Default is empty (everything). | +| maxWidth | OK | OK | To resize the image. | +| maxHeight | OK | OK | To resize the image. | +| videoQuality | OK | OK | `low`, `medium`, or `high` on iOS, `low` or `high` on Android. | +| durationLimit | OK | OK | Video max duration (in seconds). | +| quality | OK | OK | 0 to 1, photos. | +| conversionQuality | NO | OK | For conversion from HEIC/HEIF to JPEG, 0 to 1. Default is `0.92` | +| cameraType | OK | OK | 'back' or 'front' (May not be supported in few android devices). | +| includeBase64 | OK | OK | If `true`, creates base64 string of the image (Avoid using on large image files due to performance). | +| includeExtra | OK | OK | If `true`, will include extra data which requires library permissions to be requested (i.e. exif data). | +| saveToPhotos | OK | OK | (Boolean) Only for `launchCamera`, saves the image/video file captured to public photo. | +| selectionLimit | OK | OK | Supports providing any integer value. Use `0` to allow any number of files on iOS version >= 14 & Android version >= 13. Default is `1`. | +| presentationStyle | OK | NO | Controls how the picker is presented. `currentContext`, `pageSheet`, `fullScreen`, `formSheet`, `popover`, `overFullScreen`, `overCurrentContext`. Default is `currentContext`. | +| formatAsMp4 | OK | NO | Converts the selected video to MP4 (iOS Only). | +| assetRepresentationMode | OK | OK | A mode that determines which representation to use if an asset contains more than one on iOS or disables HEIC/HEIF to JPEG conversion on Android if set to 'current'. Possible values: 'auto', 'current', 'compatible'. Default is 'auto'. | | ## The Response Object -| key | iOS | Android | Web | Description | -| ------------ | --- | ------- | --- | ------------------------------------------------------------------- | -| didCancel | OK | OK | OK | `true` if the user cancelled the process | -| errorCode | OK | OK | OK | Check [ErrorCode](#ErrorCode) for all error codes | -| errorMessage | OK | OK | OK | Description of the error, use it for debug purpose only | -| assets | OK | OK | OK | Array of the selected media, [refer to Asset Object](#Asset-Object) | +| key | iOS | Android | Description | +| ------------ | --- | ------- | ------------------------------------------------------------------- | +| didCancel | OK | OK | `true` if the user cancelled the process | +| errorCode | OK | OK | Check [ErrorCode](#ErrorCode) for all error codes | +| errorMessage | OK | OK | Description of the error, use it for debug purpose only | +| assets | OK | OK | Array of the selected media, [refer to Asset Object](#Asset-Object) | ## Asset Object -| key | iOS | Android | Web | Photo/Video | Requires Permissions | Description | -| ------------ | --- | ------- | --- | ----------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| base64 | OK | OK | OK | PHOTO ONLY | NO | The base64 string of the image (photos only) | -| uri | OK | OK | OK | BOTH | NO | The file uri in app specific cache storage. Except when picking **video from Android gallery** where you will get read only content uri, to get file uri in this case copy the file to app specific storage using any react-native library. For web it uses the base64 as uri. | -| originalPath | NO | OK | NO | BOTH | NO | The original file path. | -| width | OK | OK | OK | BOTH | NO | Asset dimensions | -| height | OK | OK | OK | BOTH | NO | Asset dimensions | -| fileSize | OK | OK | NO | BOTH | NO | The file size | -| type | OK | OK | NO | BOTH | NO | The file type | -| fileName | OK | OK | NO | BOTH | NO | The file name | -| duration | OK | OK | NO | VIDEO ONLY | NO | The selected video duration in seconds | -| bitrate | --- | OK | NO | VIDEO ONLY | NO | The average bitrate (in bits/sec) of the selected video, if available. (Android only) | -| timestamp | OK | OK | NO | BOTH | YES | Timestamp of the asset. Only included if 'includeExtra' is true | -| id | OK | OK | NO | BOTH | YES | local identifier of the photo or video. On Android, this is the same as fileName | +| key | iOS | Android | Photo/Video | Requires Permissions | Description | +| ------------ | --- | ------- | ----------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| base64 | OK | OK | PHOTO ONLY | NO | The base64 string of the image (photos only) | +| uri | OK | OK | BOTH | NO | The file uri in app specific cache storage. Except when picking **video from Android gallery** where you will get read only content uri, to get file uri in this case copy the file to app specific storage using any react-native library. | +| originalPath | NO | OK | BOTH | NO | The original file path. | +| width | OK | OK | BOTH | NO | Asset dimensions | +| height | OK | OK | BOTH | NO | Asset dimensions | +| fileSize | OK | OK | BOTH | NO | The file size | +| type | OK | OK | BOTH | NO | The file type | +| fileName | OK | OK | BOTH | NO | The file name | +| duration | OK | OK | VIDEO ONLY | NO | The selected video duration in seconds | +| bitrate | --- | OK | VIDEO ONLY | NO | The average bitrate (in bits/sec) of the selected video, if available. (Android only) | +| timestamp | OK | OK | BOTH | YES | Timestamp of the asset. Only included if 'includeExtra' is true | +| id | OK | OK | BOTH | YES | local identifier of the photo or video. On Android, this is the same as fileName | ## Note on file storage Image/video captured via camera will be stored in temporary folder allowing it to be deleted any time, so don't expect it to persist. Use `saveToPhotos: true` (default is `false`) to save the file in the public photos. `saveToPhotos` requires `WRITE_EXTERNAL_STORAGE` permission on Android 28 and below (The permission has to obtained by the App manually as the library does not handle that). -For web, this doesn't work. - ## ErrorCode | Code | Description | diff --git a/example/index.web.ts b/example/index.web.ts deleted file mode 100644 index 6d6841665..000000000 --- a/example/index.web.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {AppRegistry} from 'react-native'; -import App from './src/App'; -import {name as appName} from './app.json'; - -AppRegistry.registerComponent(appName, () => App); - -AppRegistry.runApplication(appName, { - initialProps: {}, - rootTag: document.getElementById('app-root'), -}); diff --git a/package.json b/package.json index 81d83ca23..466e991fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "react-native-image-picker", - "version": "8.2.1", + "name": "@exodus/react-native-image-picker", + "version": "8.2.1-exodus-1", "description": "A React Native module that allows you to use native UI to select media from the device library or directly from the camera", "react-native": "src/index.ts", "main": "src/index.ts", @@ -14,7 +14,7 @@ "/lib" ], "author": "Johan du Toit (Johan-dutoit)", - "homepage": "https://github.com/react-native-image-picker/react-native-image-picker", + "homepage": "https://github.com/ExodusMovement/react-native-image-picker", "license": "MIT", "scripts": { "start": "react-native start", @@ -36,7 +36,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/react-native-image-picker/react-native-image-picker.git" + "url": "https://github.com/ExodusMovement/react-native-image-picker.git" }, "devDependencies": { "@react-native-community/bob": "0.17.1", diff --git a/src/index.ts b/src/index.ts index 906b6a1b2..4d7107884 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,41 @@ import {Platform} from 'react-native'; -import {CameraOptions, ImageLibraryOptions, Callback} from './types'; +import { + CameraOptions, + ImageLibraryOptions, + Callback, + ImagePickerResponse, +} from './types'; import { imageLibrary as nativeImageLibrary, camera as nativeCamera, } from './platforms/native'; -import { - imageLibrary as webImageLibrary, - camera as webCamera, -} from './platforms/web'; export * from './types'; export function launchCamera(options: CameraOptions, callback?: Callback) { - return Platform.OS === 'web' - ? webCamera(options, callback) - : nativeCamera(options, callback); + if (Platform.OS === 'web') { + const result: ImagePickerResponse = { + errorCode: 'others', + errorMessage: 'Web platform is not supported', + }; + if (callback) callback(result); + return Promise.resolve(result); + } + return nativeCamera(options, callback); } export function launchImageLibrary( options: ImageLibraryOptions, callback?: Callback, ) { - return Platform.OS === 'web' - ? webImageLibrary(options, callback) - : nativeImageLibrary(options, callback); + if (Platform.OS === 'web') { + const result: ImagePickerResponse = { + errorCode: 'others', + errorMessage: 'Web platform is not supported', + }; + if (callback) callback(result); + return Promise.resolve(result); + } + return nativeImageLibrary(options, callback); } diff --git a/src/platforms/NativeImagePicker.web.ts b/src/platforms/NativeImagePicker.web.ts deleted file mode 100644 index 23b0e3b98..000000000 --- a/src/platforms/NativeImagePicker.web.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-types */ -export interface Spec { - launchCamera(options: Object, callback: () => void): void; - launchImageLibrary(options: Object, callback: () => void): void; -} diff --git a/src/platforms/web.ts b/src/platforms/web.ts deleted file mode 100644 index f7495654d..000000000 --- a/src/platforms/web.ts +++ /dev/null @@ -1,348 +0,0 @@ -import { - CameraOptions, - ImageLibraryOptions, - Callback, - ImagePickerResponse, - ErrorCode, - Asset, - MediaType, -} from '../types'; - -const DEFAULT_OPTIONS: Pick< - ImageLibraryOptions & CameraOptions, - 'mediaType' | 'includeBase64' | 'selectionLimit' -> = { - mediaType: 'photo', - includeBase64: false, - selectionLimit: 1, -}; - -export function camera( - options: CameraOptions = DEFAULT_OPTIONS, - callback?: Callback, -): Promise { - if (options.mediaType !== 'photo') { - const result = { - errorCode: 'others' as ErrorCode, - errorMessage: 'For now, only photo mediaType is supported for web', - } - - if (callback) callback(result); - - return Promise.resolve(result); - } - - const container = document.createElement('div'); - const wrapper = document.createElement('div'); - const content = document.createElement('div'); - const buttons = document.createElement('div'); - const btnCapture = document.createElement('button'); - const btnBack = document.createElement('button'); - const btnSave = document.createElement('button'); - const btnCancel = document.createElement('button'); - const video = document.createElement('video'); - const canvas = document.createElement('canvas'); - - let currentMediaStream: MediaStream | null = null; - - // init video - navigator.mediaDevices.getUserMedia({ audio: false, video: true }) - .then(stream => { - currentMediaStream = stream; - video.srcObject = stream; - video.play(); - }).catch(err => { - console.log(err); - }) - - const isAlreadyUsingFontAwesome = !!document.getElementsByClassName('fa').length; - - if (!isAlreadyUsingFontAwesome) { - const isAlreadyInjectedFontAwesome = !!document.getElementById('injected-font-awesome'); - if (!isAlreadyInjectedFontAwesome) { - // inject font-awesome - const head = document.getElementsByTagName('HEAD')[0]; - const link = document.createElement('link'); - link.id = 'injected-font-awesome'; - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'; - head.appendChild(link); - } - } - - container.style.cssText = ` - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0,0,0,0.9); - display: flex; - align-items: center; - justify-content: center; - `; - - wrapper.style.cssText = ` - position: relative; - min-height: min(480px, 100vh); - min-width: min(640px, 100vw); - border-radius: 8px 8px 0 0; - background-color: #333333; - `; - - video.style.cssText = - canvas.style.cssText = ` - position: absolute; - top: 0; - left: 0; - border-radius: 8px 8px 0 0; - `; - - content.style.cssText = ` - display: flex; - flex-direction: column; - margin: auto; - `; - - buttons.style.cssText = ` - display: flex; - align-items: center; - justify-content: space-evenly; - min-height: 60px; - background-color: #333333; - border-radius: 0 0 8px 8px; - `; - - btnCapture.innerHTML = ''; - // btnCapture.title = 'Capture'; - btnBack.innerHTML = ''; - // btnBack.title = 'Back'; - btnSave.innerHTML = ''; - // btnSave.title = 'Apply'; - btnCancel.innerHTML = ''; - // btnCancel.title = 'Cancel'; - - btnCapture.style.cssText = - btnBack.style.cssText = - btnSave.style.cssText = - btnCancel.style.cssText = ` - padding: 10px; - color: #f2f2f2; - border: 0; - background: transparent; - `; - - wrapper.append(video); - wrapper.append(canvas); - content.append(wrapper); - content.append(buttons); - container.append(content); - - document.body.appendChild(container); - - let hasPhoto = false; - - const handleButtons = () => { - buttons.innerHTML = ''; - if (hasPhoto) { - buttons.append(btnBack); - buttons.append(btnSave); - } else { - buttons.append(btnCapture); - } - buttons.append(btnCancel); - } - - handleButtons(); - - function stopCamera() { - document.body.removeChild(container); - - if (!currentMediaStream) return; - - currentMediaStream.getTracks().forEach((track) => { - track.stop(); - }); - video.srcObject = null; - currentMediaStream = null; - } - - return new Promise((resolve) => { - btnCapture.addEventListener('click', async () => { - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - canvas.getContext('2d')?.drawImage(video, 0, 0, canvas.width, canvas.height); - hasPhoto = true; - handleButtons(); - }) - - btnBack.addEventListener('click', () => { - canvas.getContext('2d')?.clearRect(0, 0, canvas.width, canvas.height); - hasPhoto = false; - handleButtons(); - }) - - btnSave.addEventListener('click', async () => { - const uri = canvas.toDataURL('image/png'); - const asset: Asset = { uri }; - const result = {assets: [asset]}; - - if (callback) callback(result); - resolve(result); - - stopCamera(); - }) - - btnCancel.addEventListener('click', async () => { - const result = { - assets: [], - didCancel: true, - } - - if (callback) callback(result); - resolve(result); - - stopCamera(); - }) - }) -} - -export function imageLibrary( - options: ImageLibraryOptions = DEFAULT_OPTIONS, - callback?: Callback, -): Promise { - // Only supporting 'photo' mediaType for now. - if (options.mediaType !== 'photo') { - const result = { - errorCode: 'others' as ErrorCode, - errorMessage: 'For now, only photo mediaType is supported for web', - }; - - if (callback) callback(result); - - return Promise.resolve(result); - } - - const input = document.createElement('input'); - input.style.display = 'none'; - input.setAttribute('type', 'file'); - input.setAttribute('accept', getWebMediaType(options.mediaType)); - - if (options.selectionLimit! > 1) { - input.setAttribute('multiple', 'multiple'); - } - - document.body.appendChild(input); - - return new Promise((resolve) => { - const inputChangeHandler = async () => { - if (input.files) { - if (options.selectionLimit! <= 1) { - const img = await readFile(input.files[0], { - includeBase64: options.includeBase64, - }); - - const result = {assets: [img]}; - - if (callback) callback(result); - - resolve(result); - } else { - const imgs = await Promise.all( - Array.from(input.files).map((file) => - readFile(file, {includeBase64: options.includeBase64}), - ), - ); - - const result = { - didCancel: false, - assets: imgs, - }; - - if (callback) callback(result); - - resolve(result); - } - } - cleanup(); - }; - - const inputCancelHandler = async () => { - resolve({didCancel: true}); - cleanup(); - }; - - const cleanup = () => { - input.removeEventListener('change', inputChangeHandler); - input.removeEventListener('cancel', inputCancelHandler); - document.body.removeChild(input); - }; - - input.addEventListener('change', inputChangeHandler); - input.addEventListener('cancel', inputCancelHandler); - - const event = new MouseEvent('click'); - input.dispatchEvent(event); - }); -} - -function readFile( - targetFile: Blob, - options: Partial, -): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = () => { - reject( - new Error( - `Failed to read the selected media because the operation failed.`, - ), - ); - }; - reader.onload = ({target}) => { - const uri = target?.result; - - const returnRaw = () => - resolve({ - uri: uri as string, - width: 0, - height: 0, - }); - - if (typeof uri === 'string') { - const image = new Image(); - image.src = uri; - image.onload = () => - resolve({ - uri, - width: image.naturalWidth ?? image.width, - height: image.naturalHeight ?? image.height, - // The blob's result cannot be directly decoded as Base64 without - // first removing the Data-URL declaration preceding the - // Base64-encoded data. To retrieve only the Base64 encoded string, - // first remove data:*/*;base64, from the result. - // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL - ...(options.includeBase64 && { - base64: uri.substr(uri.indexOf(',') + 1), - }), - }); - image.onerror = () => returnRaw(); - } else { - returnRaw(); - } - }; - - reader.readAsDataURL(targetFile); - }); -} - -function getWebMediaType(mediaType: MediaType) { - const webMediaTypes = { - photo: 'image/*', - video: 'video/*', - mixed: 'image/*,video/*', - }; - - return webMediaTypes[mediaType] ?? webMediaTypes.photo; -}