feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,2 @@
// @generated by expo-module-scripts
module.exports = require('expo-module-scripts/eslintrc.base.js');

View File

@@ -0,0 +1,457 @@
# Changelog
## Unpublished
### 🛠 Breaking changes
### 🎉 New features
### 🐛 Bug fixes
### 💡 Others
## 15.0.7 — 2024-07-03
### 🐛 Bug fixes
- [iOS] Fix an issue where the app will crash when using the popover presentation style on iPad. ([#29892](https://github.com/expo/expo/pull/29892) by [@alanjhughes](https://github.com/alanjhughes))
## 15.0.6 — 2024-06-20
### 🐛 Bug fixes
- Support removing microphone permissions through config plugin. ([#29749](https://github.com/expo/expo/pull/29749) by [@alanjhughes](https://github.com/alanjhughes))
## 15.0.5 — 2024-05-15
### 🐛 Bug fixes
- [Android] Add support for granular permissions. ([#28897](https://github.com/expo/expo/pull/28897) by [@lukmccall](https://github.com/lukmccall))
## 15.0.4 — 2024-05-01
_This version does not introduce any user-facing changes._
## 15.0.3 — 2024-04-29
### 🎉 New features
- Add new `legacy` option to `ImagePickerOptions` to allow using the legacy image picker on android. ([#28514](https://github.com/expo/expo/pull/28514) by [@alanjhughes](https://github.com/alanjhughes))
### 🐛 Bug fixes
- Fixed type exports for isolatedModules option in typescript ([#28499](https://github.com/expo/expo/pull/28499) by [@megacherry](https://github.com/megacherry))
- On Android, fixed an issue where multiple pickers could be opened, causing a crash. ([#28509](https://github.com/expo/expo/pull/28509) by [@haileyok](https://github.com/haileyok))
## 15.0.2 — 2024-04-23
_This version does not introduce any user-facing changes._
## 15.0.1 — 2024-04-22
### 🐛 Bug fixes
- Fixed an issue where cropped images were not returning file size and file name on Android. ([#28352](https://github.com/expo/expo/pull/28352) by [@fobos531](https://github.com/fobos531))
## 15.0.0 — 2024-04-18
### 🎉 New features
- Add ability to disable permissions in config plugin by passing `false` instead of permission messages. ([#28107](https://github.com/expo/expo/pull/28107) by [@EvanBacon](https://github.com/EvanBacon))
### 🐛 Bug fixes
- On Android `fileSize` was named `filesize` which did not match the docs & typescript definition. ([#27293](https://github.com/expo/expo/pull/27293) by [@WookieFPV](https://github.com/wookieFPV))
### 💡 Others
- drop unused web `name` property. ([#27437](https://github.com/expo/expo/pull/27437) by [@EvanBacon](https://github.com/EvanBacon))
- Convert WEBP to PNG instead JPEG when selecting an item in the Media Library with editing enabled. ([#26419](https://github.com/expo/expo/pull/26419) by [@NikitaDudin](https://github.com/NikitaDudin))
- Receiving a correct file extension for WEBP files instead `.jpeg` in the ImagePicker result. ([#26419](https://github.com/expo/expo/pull/26419) by [@NikitaDudin](https://github.com/NikitaDudin))
- Removed deprecated backward compatible Gradle settings. ([#28083](https://github.com/expo/expo/pull/28083) by [@kudo](https://github.com/kudo))
## 14.7.1 - 2023-12-19
_This version does not introduce any user-facing changes._
## 14.7.0 — 2023-11-14
### 🛠 Breaking changes
- Bumped iOS deployment target to 13.4. ([#25063](https://github.com/expo/expo/pull/25063) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- On `Android` bump `compileSdkVersion` and `targetSdkVersion` to `34`. ([#24708](https://github.com/expo/expo/pull/24708) by [@alanjhughes](https://github.com/alanjhughes))
## 14.6.0 — 2023-10-17
### 🛠 Breaking changes
- Dropped support for Android SDK 21 and 22. ([#24201](https://github.com/expo/expo/pull/24201) by [@behenate](https://github.com/behenate))
### 🎉 New features
- On Android, support `fileName` and `filesize` in the returned assets. ([#24524](https://github.com/expo/expo/pull/24524) by [@alanjhughes](https://github.com/alanjhughes))
- Support returning the mime type of the returned assets. ([#24659](https://github.com/expo/expo/pull/24659) by [@alanjhughes](https://github.com/alanjhughes))
## 14.5.0 — 2023-09-04
### 🎉 New features
- Added support for React Native 0.73. ([#24018](https://github.com/expo/expo/pull/24018) by [@kudo](https://github.com/kudo))
### 🐛 Bug fixes
- On Android, the `Uri` type is not serializable and causes a crash when recreating the activity. ([#23768](https://github.com/expo/expo/pull/23768) by [@alanjhughes](https://github.com/alanjhughes))
### 💡 Others
- Remove deprecation warnings on `ImagePickerResult`. ([#24226](https://github.com/expo/expo/pull/24226) by [@alanjhughes](https://github.com/alanjhughes))
## 14.4.1 — 2023-08-02
_This version does not introduce any user-facing changes._
## 14.4.0 — 2023-07-28
_This version does not introduce any user-facing changes._
## 14.3.2 - 2023-07-23
### 💡 Others
- On Android, restore behavior from [#22658](https://github.com/expo/expo/pull/22658). ([#23617](https://github.com/expo/expo/pull/23617) by [@alanhughes](https://github.com/alanjhughes)) ([#22658](https://github.com/expo/expo/pull/22658), [#23617](https://github.com/expo/expo/pull/23617) by [@alanjhughes](https://github.com/alanjhughes))
## 14.3.1 - 2023-07-04
### 🐛 Bug fixes
- Fix manifest merger build fail on Android. ([#23191](https://github.com/expo/expo/pull/23191) by [@alexandrius](https://github.com/alexandrius))
- [Android] Fix backported photo picker crashing with null intent. ([#23224](https://github.com/expo/expo/pull/23224) by [@thespacemanatee](https://github.com/thespacemanatee))
## 14.3.0 — 2023-06-13
### 🎉 New features
- Added ability to choose the preferred asset representation mode on iOS 14+. ([#22456](https://github.com/expo/expo/pull/22456) by [@thespacemanatee](https://github.com/thespacemanatee))
- Updated the Android image picker to use a [more streamlined and modern interface](https://developer.android.com/training/data-storage/shared/photopicker), closely resembling the one on iOS. [#22658](https://github.com/expo/expo/pull/22658) by [@fobos531](https://github.com/fobos531)
### 🐛 Bug fixes
- Fixed Android build warnings for Gradle version 8. ([#22537](https://github.com/expo/expo/pull/22537), [#22609](https://github.com/expo/expo/pull/22609) by [@kudo](https://github.com/kudo))
- Fixed an issue that allowed picking non-image/video files when passing `MediaTypeOptions.All` ([#22606](https://github.com/expo/expo/pull/22606) by [@fobos531](https://github.com/fobos531))
## 14.2.0 — 2023-05-08
### 🎉 New features
- Added ability to choose the camera-facing type. ([#22143](https://github.com/expo/expo/pull/22143) by [@YoussefHenna](https://github.com/YoussefHenna))
### 🐛 Bug fixes
- Fix images unexpectedly being converted to `.png` when opening `.bmp` files and selecting any quality in `ImagePickerOptions`. ([#21361](https://github.com/expo/expo/pull/21361) by [@behenate](https://github.com/behenate))
- Fix issue where the array of permissions could end up empty causing an exception. ([#21589](https://github.com/expo/expo/pull/21589) by [@alanhughes](https://github.com/alanjhughes))
- Fix rotated videos returning incorrect width/height. [#12573](https://github.com/expo/expo/issues/12573) ([#21758](https://github.com/expo/expo/pull/21758) by [@mmmulani](https://github.com/mmmulani))
- Fix NullPointerException for launchCameraAsync on Android 13. ([#22123](https://github.com/expo/expo/pull/22123) by [@witheroux](https://github.com/witheroux))
- [Android] Fix image picker returning inverted dimensions when selecting vertical images without editing. ([#22383](https://github.com/expo/expo/pull/22383) by [@behenate](https://github.com/behenate))
## 14.1.1 — 2023-02-09
### ⚠️ Notices
- Removed deprecated fields from pick result type and deprecated `UIImagePickerPresentationStyle` enum values. ([#21078](https://github.com/expo/expo/pull/21078) by [@Simek](https://github.com/Simek))
## 14.1.0 — 2023-01-26
### 🎉 New features
- Add support for [granular permissions](https://developer.android.com/about/versions/13/behavior-changes-13) on Android 13. ([#20908](https://github.com/expo/expo/pull/20908) by [@alanhughes](https://github.com/alanjhughes))
### 💡 Others
- On Android bump `compileSdkVersion` and `targetSdkVersion` to `33`. ([#20721](https://github.com/expo/expo/pull/20721) by [@lukmccall](https://github.com/lukmccall))
## 14.0.3 — 2022-12-30
### 💡 Others
- Avoid dependency on `uuid`. ([#20476](https://github.com/expo/expo/pull/20476) by [@LinusU](https://github.com/LinusU))
## 14.0.2 - 2022-11-21
### 🐛 Bug fixes
- Fix support for animated GIFs on iOS. ([#20034](https://github.com/expo/expo/pull/20034) by [@barthap](https://github.com/barthap))
## 14.0.1 - 2022-11-08
### 🐛 Bug fixes
- Fix incorrect asset type for videos on iOS. ([#19932](https://github.com/expo/expo/pull/19932) by [@tsapeta](https://github.com/tsapeta))
## 14.0.0 — 2022-10-25
### 🛠 Breaking changes
- Remove deprecated `requestCameraRollPermissionsAsync` and `getCameraRollPermissionsAsync` methods, as well as associated to them `CameraRollPermissionResponse` type. ([#18600](https://github.com/expo/expo/pull/18600) by [@Simek](https://github.com/Simek))
- Bumped iOS deployment target to 13.0 and deprecated support for iOS 12. ([#18873](https://github.com/expo/expo/pull/18873) by [@tsapeta](https://github.com/tsapeta))
- Reworked the picking result object for simplicity and better compatibility with the multiple selection. ([#19570](https://github.com/expo/expo/pull/19570) by [@tsapeta](https://github.com/tsapeta))
### 🎉 New features
- The new `PHPickerViewController` is now default picker interface on iOS 14+. ([#18871](https://github.com/expo/expo/pull/18871) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix images taken with `launchCameraAsync` being translated incorrectly on some camera orientations. ([#19185](https://github.com/expo/expo/pull/19185) by [@jacobjaffe](https://github.com/JacobJaffe) and [@reececox](https://github.com/reececox))
- Fix error where `launchImageLibraryAsync()` saved the photo to a global cache directory that was inaccessible in Expo Go. ([#19205](https://github.com/expo/expo/pull/19205) by [@aleqsio](https://github.com/aleqsio))
### 💡 Others
- Drop `@expo/config-plugins` dependency in favor of peer dependency on `expo`. ([#18595](https://github.com/expo/expo/pull/18595) by [@EvanBacon](https://github.com/EvanBacon))
## 13.3.1 — 2022-07-25
_This version does not introduce any user-facing changes._
## 13.3.0 — 2022-07-16
### 🎉 New features
- On iOS 14+ added support for selection limit and on iOS 15+ for ordered selection. ([#18142](https://github.com/expo/expo/pull/18142), [#18143](https://github.com/expo/expo/pull/18143) by [@barthap](https://github.com/barthap))
- The picker now resolves media library asset ID. ([#18236](https://github.com/expo/expo/pull/18236), [#18185](https://github.com/expo/expo/pull/18185) by [@barthap](https://github.com/barthap))
- On iOS, the picker now resolves file name and size and media library asset ID. ([#18179](https://github.com/expo/expo/pull/18179) by [@barthap](https://github.com/barthap))
- On Android added support for selecting multiple images/videos. ([#18161](https://github.com/expo/expo/pull/18161) by [@bbarthec](https://github.com/bbarthec))
### 🐛 Bug fixes
- On Android restored support for `allowsEditing` option that was disabled when migrating to `registerForActivityResult` mechanism. ([#17963](https://github.com/expo/expo/pull/17963) by [@bbarthec](https://github.com/bbarthec))
## 13.2.1 — 2022-07-11
_This version does not introduce any user-facing changes._
## 13.2.0 — 2022-07-07
### 🎉 New features
- On iOS 14+ added support for selecting multiple images/videos. ([#18102](https://github.com/expo/expo/pull/18102), [#18138](https://github.com/expo/expo/pull/18138), [#18145](https://github.com/expo/expo/pull/18145) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes
- Fix crash when picking a GIF image on iOS. ([#18135](https://github.com/expo/expo/pull/18135) by [@barthap](https://github.com/barthap))
### 💡 Others
- On Android migrated to the new `registerForActivityResult` mechanism. This migration disables `allowsEditing` option. ([#17671](https://github.com/expo/expo/pull/17671), ([#17987](https://github.com/expo/expo/pull/17987) by [@bbarthec](https://github.com/bbarthec))
- Native module on Android is now written in Kotlin using [Sweet API](https://docs.expo.dev/modules/module-api). ([#17668](https://github.com/expo/expo/pull/17668) by [@bbarthec](https://github.com/bbarthec))
- Migrated Expo modules definitions to the new naming convention. ([#17193](https://github.com/expo/expo/pull/17193) by [@tsapeta](https://github.com/tsapeta))
## 13.1.1 — 2022-04-27
_This version does not introduce any user-facing changes._
## 13.1.0 — 2022-04-25
### 🎉 New features
- [plugin] Added ability to disable permissions. ([#17168](https://github.com/expo/expo/pull/17168) by [@EvanBacon](https://github.com/EvanBacon))
## 13.0.1 — 2022-04-20
_This version does not introduce any user-facing changes._
## 13.0.0 — 2022-04-18
### 🛠 Breaking changes
- On Android migrated cropping library from `com.theartofdev.edmodo:android-image-cropper@2.8.0` (available from `jcenter()`) to `com.github.CanHub:Android-Image-Cropper@1.1.1` (available from `jitpack.io`). ([#11647](https://github.com/expo/expo/pull/11647) by [@bbarthec](https://github.com/bbarthec))
### 🎉 New features
- Native module on iOS is now written in Swift using the new API. ([#15977](https://github.com/expo/expo/pull/15977) by [@bbarthec](https://github.com/bbarthec))
### 🐛 Bug fixes
- Fixed crashes on Android after image is picked by adding missing dependency `expo-image-loader`. ([#17019](https://github.com/expo/expo/pull/17019) by [@M1ST4KE](https://github.com/M1ST4KE))
- Fix failure on Android when `allowsEditing` is `true` and non-jpeg file picked. ([#16615](https://github.com/expo/expo/pull/16615) by [@mnightingale](https://github.com/mnightingale))
### ⚠ Notices
- Deleted the `UIImagePickerPresentationStyle.BlurOverFullScreen` option as it does not work on iOS. ([#16925](https://github.com/expo/expo/pull/16925) by [@barthap](https://github.com/barthap))
- Deprecated all `PascalCase` values of the `UIImagePickerPresentationStyle` enum. Use their `SNAKE_UPPERCASE` counterparts instead. ([#16925](https://github.com/expo/expo/pull/16925) by [@barthap](https://github.com/barthap))
- Underlying values of the `UIImagePickerPresentationStyle` are now strings. They were integers before. ([#16925](https://github.com/expo/expo/pull/16925) by [@barthap](https://github.com/barthap))
### 💡 Others
- Updated `@expo/config-plugins` from `4.0.2` to `4.0.14` ([#15621](https://github.com/expo/expo/pull/15621) by [@EvanBacon](https://github.com/EvanBacon))
- Export missing `PermissionResponse` type. ([#15744](https://github.com/expo/expo/pull/15744) by [@Simek](https://github.com/Simek))
### ⚠️ Notices
- On Android bump `compileSdkVersion` to `31`, `targetSdkVersion` to `31` and `Java` version to `11`. ([#16941](https://github.com/expo/expo/pull/16941) by [@bbarthec](https://github.com/bbarthec))
## 12.0.2 - 2022-02-01
### 🐛 Bug fixes
- Fix `Plugin with id 'maven' not found` build error from Android Gradle 7. ([#16080](https://github.com/expo/expo/pull/16080) by [@kudo](https://github.com/kudo))
## 12.0.1 — 2021-12-15
### 🐛 Bug fixes
- Fix unresolved promise when picker was dismissed with a swipe-down on iOS. ([#15511](https://github.com/expo/expo/pull/15511) by [@barthap](https://github.com/barthap))
## 12.0.0 — 2021-12-03
### 🛠 Breaking changes
- Remove typo workaround for `getMediaLibaryPermissionsAsync` method. ([#14646](https://github.com/expo/expo/pull/14646) by [@Simek](https://github.com/Simek))
### 🐛 Bug fixes
- On Web add missing `cancelled` property to the return values of `launchCameraAsync` and `launchImageLibraryAsync` methods. ([#14646](https://github.com/expo/expo/pull/14646) by [@Simek](https://github.com/Simek))
### 💡 Others
- Export missing types: `ImageInfo`, `ImagePickerMultipleResult`, `OpenFileBrowserOptions`, `ExpandImagePickerResult`, `UIImagePickerControllerQualityType` and `UIImagePickerPresentationStyle`. ([#14646](https://github.com/expo/expo/pull/14646) by [@Simek](https://github.com/Simek))
- Extract return object containing `{ cancelled: true }` to separate type `ImagePickerCancelledResult` for `launchCameraAsync` and `launchImageLibraryAsync` methods. ([#14646](https://github.com/expo/expo/pull/14646) by [@Simek](https://github.com/Simek))
## 11.0.1 — 2021-10-01
_This version does not introduce any user-facing changes._
## 11.0.0 — 2021-09-28
### 🛠 Breaking changes
- Dropped support for iOS 11.0 ([#14383](https://github.com/expo/expo/pull/14383) by [@cruzach](https://github.com/cruzach))
### 🎉 New features
- Add useCameraPermissions and useMediaLibraryPermissions hooks from modules factory. ([#13859](https://github.com/expo/expo/pull/13859) by [@bycedric](https://github.com/bycedric))
- Add possibility to change presentation style on iOS. ([#14068](https://github.com/expo/expo/pull/14068) by [@mstach60161](https://github.com/mstach60161))
### 🐛 Bug fixes
- Add missing `GPSHPositioningError` exif parameter on Android. ([#13998](https://github.com/expo/expo/pull/13998) by [@mstach60161](https://github.com/mstach60161))
- Fix promise not resolving when the app is moved to the background on Android. ([#13975](https://github.com/expo/expo/pull/13975) by [@mstach60161](https://github.com/mstach60161))
- Fix building errors from use_frameworks! in Podfile. ([#14523](https://github.com/expo/expo/pull/14523) by [@kudo](https://github.com/kudo))
### 💡 Others
- Updated `@expo/config-plugins` ([#14443](https://github.com/expo/expo/pull/14443) by [@EvanBacon](https://github.com/EvanBacon))
## 10.2.0 — 2021-06-16
### 🐛 Bug fixes
- Fixed `base64` return on web. ([#12529](https://github.com/expo/expo/pull/12529) by [@simonezuccala](https://github.com/simonezuccala) and [@misterdev](https://github.com/misterdev))
- Enable kotlin in all modules. ([#12716](https://github.com/expo/expo/pull/12716) by [@wschurman](https://github.com/wschurman))
- Fixed cases where Picker & Camera would be transparent on iOS 14.5. ([#12897](https://github.com/expo/expo/pull/12897) by [@cruzach](https://github.com/cruzach))
- Add missing exif data to cropped image on Android. ([#14038](https://github.com/expo/expo/pull/14038) by [@mstach60161](https://github.com/mstach60161))
### 💡 Others
- Migrated from `unimodules-file-system-interface` and `unimodules-permissions-interface` to `expo-modules-core`. ([#12961](https://github.com/expo/expo/pull/12961) by [@tsapeta](https://github.com/tsapeta))
- Migrated from `AsyncTask` to Kotlin concurrency utilities. ([#13800](https://github.com/expo/expo/pull/13800) by [@m1st4ke](https://github.com/m1st4ke))
## 10.1.3 — 2021-04-13
_This version does not introduce any user-facing changes._
## 10.1.2 — 2021-04-09
### 🐛 Bug fixes
- Added SSR guard. ([#12420](https://github.com/expo/expo/pull/12420) by [@EvanBacon](https://github.com/EvanBacon))
- Reverted focus state listener PR. ([#12420](https://github.com/expo/expo/pull/12420) by [@EvanBacon](https://github.com/EvanBacon))
## 10.1.1 — 2021-03-31
_This version does not introduce any user-facing changes._
## 10.1.0 — 2021-03-10
### 🎉 New features
- Converted plugin to TypeScript. ([#11715](https://github.com/expo/expo/pull/11715) by [@EvanBacon](https://github.com/EvanBacon))
- Updated Android build configuration to target Android 11 (added support for Android SDK 30). ([#11647](https://github.com/expo/expo/pull/11647) by [@bbarthec](https://github.com/bbarthec))
### 🐛 Bug fixes
- Fixed `launchCameraAsync()` with `allowsEditing` option crashing for some android users. ([#11825](https://github.com/expo/expo/pull/11825) by [@lukmccall](https://github.com/lukmccall))
- Fixed cancelled picker dialog not resolving with expected result on web. ([#11847](https://github.com/expo/expo/pull/11847) by [@jayprado](https://github.com/jayprado))
- Fixed incorrect file URI on Android. ([#11823](https://github.com/expo/expo/pull/11823) by [@lukmccall](https://github.com/lukmccall))
## 10.0.0 — 2021-01-15
### 🛠 Breaking changes
- Dropped support for iOS 10.0 ([#11344](https://github.com/expo/expo/pull/11344) by [@tsapeta](https://github.com/tsapeta))
### 🎉 New features
- Created config plugins ([#11538](https://github.com/expo/expo/pull/11538) by [@EvanBacon](https://github.com/EvanBacon))
### 🐛 Bug fixes
- Fixed possible unsafe call in VideoResultTask. ([#11552](https://github.com/expo/expo/pull/11552) by [@Duell10111](https://github.com/Duell10111))
## 9.2.1 — 2020-12-09
### 🐛 Bug fixes
- Fix typo in media library permission methods. ([#11292](https://github.com/expo/expo/pull/11292) by [@bycedric](https://github.com/bycedric))
## 9.2.0 — 2020-11-17
_This version does not introduce any user-facing changes._
## 9.1.1 — 2020-09-23
### 🐛 Bug fixes
- `launchImageLibraryAsync()` should be callable with no options argument ([#10306](https://github.com/expo/expo/pull/10306))
## 9.1.0 — 2020-08-18
### 🎉 New features
- Added a way to handle results when activity was killed by the android. ([#9697](https://github.com/expo/expo/pull/9697) by [@lukmccall](https://github.com/lukmccall))
### 🐛 Bug fixes
- Return array of `ImagePickerResult` when `allowsMultipleSelection` is set to `true` on Web. ([#9402](https://github.com/expo/expo/pull/9402) by [@isthaison](https://github.com/isthaison))
- video recorded on iOS recorded was producing [low resolution](https://github.com/expo/expo/issues/6224) videos, not it's fixed, and can be configured via the `videoQuality` option. ([#9808](https://github.com/expo/expo/pull/9808) by [@vujevits](https://github.com/vujevits))
## 9.0.0 — 2020-08-11
### 🛠 Breaking changes
- Added camera and external storage permissions declarations to `AndroidManifest.xml` on Android. ([#9230](https://github.com/expo/expo/pull/9230) by [@bycedric](https://github.com/bycedric))
### 🎉 New features
- Added support for the limited `CAMERA_ROLL` permission on iOS 14. ([#9423](https://github.com/expo/expo/pull/9423) by [@lukmccall](https://github.com/lukmccall))
- Added `videoMaxDuration` option to `launchCameraAsync()` to configure video recording duration limit. ([#9486](https://github.com/expo/expo/pull/9486) by [@barthap](https://github.com/barthap))
## 8.4.0 — 2020-07-27
### 🐛 Bug fixes
- Fixed downsizing cropped image, when `allowsEditing` was `true`. ([#9316](https://github.com/expo/expo/pull/9316) by [@barthap](https://github.com/barthap))
## 8.3.0 — 2020-05-29
_This version does not introduce any user-facing changes._
## 8.2.0 — 2020-05-27
### 🛠 Breaking changes
- The base64 output will no longer contain newline and special character (`\n`, `\r`) on Android. ([#7841](https://github.com/expo/expo/pull/7841) by [@jarvisluong](https://github.com/jarvisluong))
### 🐛 Bug fixes
- Skip asking for camera permissions on web with `ImagePicker.getCameraPermissionsAsync`. ([#8475](https://github.com/expo/expo/pull/8475) by [@EvanBacon](https://github.com/EvanBacon))
- Fixed exception when calling `ImagePicker.getCameraPermissionsAsync` on Web. ([#7498](https://github.com/expo/expo/pull/7498) by [@IjzerenHein](https://github.com/IjzerenHein))

View File

@@ -0,0 +1,108 @@
<p>
<a href="https://docs.expo.dev/versions/latest/sdk/imagepicker/">
<img
src="../../.github/resources/expo-image-picker.svg"
alt="expo-image-picker"
height="64" />
</a>
</p>
Provides access to the system's UI for selecting images and videos from the phone's library or taking a photo with the camera.
# API documentation
- [Documentation for the main branch](https://github.com/expo/expo/blob/main/docs/pages/versions/unversioned/sdk/imagepicker.mdx)
- [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/imagepicker/)
# Installation in managed Expo projects
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/imagepicker/).
# Installation in bare React Native projects
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
### Add the package to your npm dependencies
```
npx expo install expo-image-picker
```
### Configure for iOS
> This is only required for usage in bare React Native apps.
Add `NSPhotoLibraryUsageDescription`, `NSCameraUsageDescription`, and `NSMicrophoneUsageDescription` keys to your `Info.plist`:
```xml
<key>NSPhotoLibraryUsageDescription</key>
<string>Give $(PRODUCT_NAME) permission to save photos</string>
<key>NSCameraUsageDescription</key>
<string>Give $(PRODUCT_NAME) permission to access your camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>Give $(PRODUCT_NAME) permission to use your microphone</string>
```
Run `npx pod-install` after installing the npm package.
### Configure for Android
> This is only required for usage in bare React Native apps.
This package automatically adds the `CAMERA`, `READ_EXTERNAL_STORAGE`, and `WRITE_EXTERNAL_STORAGE` permissions. They are used when picking images from the camera directly, or from the camera roll.
```xml
<!-- Added permissions -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```
## Config Plugin
> This plugin is applied automatically in EAS Build, only add the config plugin if you want to pass in extra properties.
After installing this npm package, add the [config plugin](https://docs.expo.dev/config-plugins/introduction) to the [`plugins`](https://docs.expo.io/versions/latest/config/app/#plugins) array of your `app.json` or `app.config.js`:
```json
{
"expo": {
"plugins": ["expo-image-picker"]
}
}
```
Next, rebuild your app as described in the ["Adding custom native code"](https://docs.expo.dev/workflow/customizing/) guide.
### API
The plugin provides props for extra customization. Every time you change the props or plugins, you'll need to rebuild (and `prebuild`) the native app. If no extra properties are added, defaults will be used.
- `photosPermission` (_string | false_): Sets the iOS `NSPhotoLibraryUsageDescription` permission message to the `Info.plist`. Setting `false` will skip adding the permission on iOS and **does not** skip the permission on Android. Defaults to `Allow $(PRODUCT_NAME) to access your photos`.
- `cameraPermission` (_string | false_): Sets the iOS `NSCameraUsageDescription` permission message to the `Info.plist`. Setting `false` will skip adding the permission on iOS and **does not** skip the permission on Android. Defaults to `Allow $(PRODUCT_NAME) to access your camera`.
- `microphonePermission` (_string | false_): Sets the iOS `NSCameraUsageDescription` permission message to the `Info.plist`. Setting `false` will skip adding the permission on iOS and skips adding the `android.permission.RECORD_AUDIO` Android permission. Defaults to `Allow $(PRODUCT_NAME) to access your photos`.
### Example
```json
{
"expo": {
"plugins": [
[
"expo-image-picker",
{
"photosPermission": "custom photos permission",
"cameraPermission": "Allow $(PRODUCT_NAME) to open the camera",
"//": "Disables the microphone permission",
"microphonePermission": false
}
]
]
}
}
```
# Contributing
Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).

View File

@@ -0,0 +1,28 @@
apply plugin: 'com.android.library'
group = 'host.exp.exponent'
version = '15.0.7'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useDefaultAndroidSdkVersions()
useExpoPublishing()
android {
namespace "expo.modules.imagepicker"
defaultConfig {
versionCode 22
versionName "15.0.7"
}
}
dependencies {
implementation "androidx.activity:activity-ktx:1.7.2"
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.exifinterface:exifinterface:1.3.3"
implementation "com.github.CanHub:Android-Image-Cropper:4.3.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3"
}

View File

@@ -0,0 +1,51 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Required for picking images from camera directly -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Required for picking images from camera roll -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<application>
<service
android:name="com.google.android.gms.metadata.ModuleDependencies"
android:enabled="false"
android:exported="false"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
</intent-filter>
<meta-data
android:name="photopicker_activity:0:required"
android:value="" />
</service>
<activity
android:name="com.canhub.cropper.CropImageActivity"
android:theme="@style/Base.Theme.AppCompat" />
<!-- https://developer.android.com/guide/topics/manifest/provider-element.html -->
<provider
android:name=".fileprovider.ImagePickerFileProvider"
android:authorities="${applicationId}.ImagePickerFileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/image_picker_provider_paths" />
</provider>
</application>
<queries>
<intent>
<!-- Required for picking images from the camera roll if targeting API 30 -->
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
<intent>
<!-- Required for picking images from the camera if targeting API 30 -->
<action android:name="android.media.action.ACTION_VIDEO_CAPTURE" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,164 @@
package expo.modules.imagepicker
import androidx.exifinterface.media.ExifInterface
object ImagePickerConstants {
const val TAG = "ExponentImagePicker"
const val MAXIMUM_QUALITY = 1.0
const val CACHE_DIR_NAME = "ImagePicker"
/**
* Expose List<Pair<Type, Exif>> as [Iterable] for easier access.
*/
val EXIF_TAGS = object : Iterable<Pair<String, String>> {
/**
* Map { "string" | "double" | "int" -> List<String> } into List<"string" | "double" | "int", String>
*/
override fun iterator(): Iterator<Pair<String, String>> =
typeToTags
.flatMap { (type, tags) -> tags.map { tag -> type to tag } }
.iterator()
private val typeToTags = mapOf(
"string" to listOf(
ExifInterface.TAG_ARTIST,
ExifInterface.TAG_CFA_PATTERN,
ExifInterface.TAG_COMPONENTS_CONFIGURATION,
ExifInterface.TAG_COPYRIGHT,
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_DATETIME_DIGITIZED,
ExifInterface.TAG_DATETIME_ORIGINAL,
ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
ExifInterface.TAG_EXIF_VERSION,
ExifInterface.TAG_FILE_SOURCE,
ExifInterface.TAG_FLASHPIX_VERSION,
ExifInterface.TAG_GPS_AREA_INFORMATION,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_GPS_DEST_BEARING_REF,
ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
ExifInterface.TAG_GPS_H_POSITIONING_ERROR,
ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_MAP_DATUM,
ExifInterface.TAG_GPS_MEASURE_MODE,
ExifInterface.TAG_GPS_PROCESSING_METHOD,
ExifInterface.TAG_GPS_SATELLITES,
ExifInterface.TAG_GPS_SPEED_REF,
ExifInterface.TAG_GPS_STATUS,
ExifInterface.TAG_GPS_TIMESTAMP,
ExifInterface.TAG_GPS_TRACK_REF,
ExifInterface.TAG_GPS_VERSION_ID,
ExifInterface.TAG_IMAGE_DESCRIPTION,
ExifInterface.TAG_IMAGE_UNIQUE_ID,
ExifInterface.TAG_INTEROPERABILITY_INDEX,
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MAKER_NOTE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_OECF,
ExifInterface.TAG_RELATED_SOUND_FILE,
ExifInterface.TAG_SCENE_TYPE,
ExifInterface.TAG_SOFTWARE,
ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
ExifInterface.TAG_SPECTRAL_SENSITIVITY,
ExifInterface.TAG_SUBSEC_TIME,
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
ExifInterface.TAG_USER_COMMENT
),
"double" to listOf(
ExifInterface.TAG_APERTURE_VALUE,
ExifInterface.TAG_BRIGHTNESS_VALUE,
ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
ExifInterface.TAG_EXPOSURE_INDEX,
ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_FLASH_ENERGY,
ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
ExifInterface.TAG_F_NUMBER,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_GPS_DEST_BEARING,
ExifInterface.TAG_GPS_DEST_DISTANCE,
ExifInterface.TAG_GPS_DEST_LATITUDE,
ExifInterface.TAG_GPS_DEST_LONGITUDE,
ExifInterface.TAG_GPS_DOP,
ExifInterface.TAG_GPS_IMG_DIRECTION,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_SPEED,
ExifInterface.TAG_GPS_TRACK,
ExifInterface.TAG_MAX_APERTURE_VALUE,
ExifInterface.TAG_PRIMARY_CHROMATICITIES,
ExifInterface.TAG_REFERENCE_BLACK_WHITE,
ExifInterface.TAG_SHUTTER_SPEED_VALUE,
ExifInterface.TAG_SUBJECT_DISTANCE,
ExifInterface.TAG_WHITE_POINT,
ExifInterface.TAG_X_RESOLUTION,
ExifInterface.TAG_Y_CB_CR_COEFFICIENTS,
ExifInterface.TAG_Y_RESOLUTION
),
"int" to listOf(
ExifInterface.TAG_BITS_PER_SAMPLE,
ExifInterface.TAG_COLOR_SPACE,
ExifInterface.TAG_COMPRESSION,
ExifInterface.TAG_CONTRAST,
ExifInterface.TAG_CUSTOM_RENDERED,
ExifInterface.TAG_DEFAULT_CROP_SIZE,
ExifInterface.TAG_DNG_VERSION,
ExifInterface.TAG_EXPOSURE_MODE,
ExifInterface.TAG_EXPOSURE_PROGRAM,
ExifInterface.TAG_FLASH,
ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
ExifInterface.TAG_GAIN_CONTROL,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_DIFFERENTIAL,
ExifInterface.TAG_IMAGE_LENGTH,
ExifInterface.TAG_IMAGE_WIDTH,
ExifInterface.TAG_ISO_SPEED_RATINGS,
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
ExifInterface.TAG_LIGHT_SOURCE,
ExifInterface.TAG_METERING_MODE,
ExifInterface.TAG_NEW_SUBFILE_TYPE,
ExifInterface.TAG_ORF_ASPECT_FRAME,
ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH,
ExifInterface.TAG_ORF_PREVIEW_IMAGE_START,
ExifInterface.TAG_ORIENTATION,
ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
ExifInterface.TAG_PIXEL_X_DIMENSION,
ExifInterface.TAG_PIXEL_Y_DIMENSION,
ExifInterface.TAG_PLANAR_CONFIGURATION,
ExifInterface.TAG_RESOLUTION_UNIT,
ExifInterface.TAG_ROWS_PER_STRIP,
ExifInterface.TAG_RW2_ISO,
ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER,
ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER,
ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER,
ExifInterface.TAG_RW2_SENSOR_TOP_BORDER,
ExifInterface.TAG_SAMPLES_PER_PIXEL,
ExifInterface.TAG_SATURATION,
ExifInterface.TAG_SCENE_CAPTURE_TYPE,
ExifInterface.TAG_SENSING_METHOD,
ExifInterface.TAG_SHARPNESS,
ExifInterface.TAG_STRIP_BYTE_COUNTS,
ExifInterface.TAG_STRIP_OFFSETS,
ExifInterface.TAG_SUBFILE_TYPE,
ExifInterface.TAG_SUBJECT_AREA,
ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
ExifInterface.TAG_SUBJECT_LOCATION,
ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH,
ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH,
ExifInterface.TAG_TRANSFER_FUNCTION,
ExifInterface.TAG_WHITE_BALANCE,
ExifInterface.TAG_Y_CB_CR_POSITIONING,
ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING
)
)
}
}

View File

@@ -0,0 +1,38 @@
package expo.modules.imagepicker
import androidx.core.net.toUri
import expo.modules.kotlin.exception.CodedException
import java.io.File
internal class FailedToDeduceTypeException :
CodedException("Can not deduce type of the returned file")
internal class FailedToCreateFileException(path: String, cause: Throwable? = null) :
CodedException("Failed to create the file '$path'", cause)
internal class FailedToPickMediaException :
CodedException("Failed to parse PhotoPicker result")
internal class FailedToExtractVideoMetadataException(file: File? = null, cause: Throwable? = null) :
CodedException("Failed to extract metadata from video file '${file?.toUri()?.toString() ?: ""}'", cause)
internal class FailedToWriteExifDataToFileException(file: File, cause: Throwable) :
CodedException("Failed to write EXIF data to file '${file.toUri()}", cause)
internal class FailedToWriteFileException(file: File? = null, cause: Throwable? = null) :
CodedException("Failed to write a file '${file?.toUri()?.toString() ?: ""}'", cause)
internal class FailedToReadFileException(file: File, cause: Throwable? = null) :
CodedException("Failed to read a file '${file.toUri()}", cause)
internal class MissingActivityToHandleIntent(intentType: String?) :
CodedException("Failed to resolve activity to handle the intent of type '$intentType'")
internal class MissingCurrentActivityException :
CodedException("Activity which was provided during module initialization is no longer available")
internal class MissingModuleException(moduleName: String) :
CodedException("Module '$moduleName' not found. Are you sure all modules are linked correctly?")
internal class UserRejectedPermissionsException :
CodedException("User rejected permissions")

View File

@@ -0,0 +1,310 @@
package expo.modules.imagepicker
import android.Manifest
import android.Manifest.permission.READ_MEDIA_IMAGES
import android.Manifest.permission.READ_MEDIA_VIDEO
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.OperationCanceledException
import androidx.core.content.ContextCompat
import expo.modules.core.errors.ModuleNotFoundException
import expo.modules.imagepicker.contracts.CameraContract
import expo.modules.imagepicker.contracts.CameraContractOptions
import expo.modules.imagepicker.contracts.CropImageContract
import expo.modules.imagepicker.contracts.CropImageContractOptions
import expo.modules.imagepicker.contracts.ImageLibraryContract
import expo.modules.imagepicker.contracts.ImageLibraryContractOptions
import expo.modules.imagepicker.contracts.ImagePickerContractResult
import expo.modules.interfaces.permissions.Permissions
import expo.modules.interfaces.permissions.PermissionsResponse
import expo.modules.interfaces.permissions.PermissionsResponseListener
import expo.modules.interfaces.permissions.PermissionsStatus
import expo.modules.kotlin.Promise
import expo.modules.kotlin.activityresult.AppContextActivityResultLauncher
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.functions.Coroutine
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.weak
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.File
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
// TODO(@bbarthec): rename to ExpoImagePicker
private const val moduleName = "ExponentImagePicker"
const val ACCESS_PRIVILEGES_PERMISSION_KEY = "accessPrivileges"
class ImagePickerModule : Module() {
override fun definition() = ModuleDefinition {
Name(moduleName)
// region JS API
AsyncFunction("requestMediaLibraryPermissionsAsync") { writeOnly: Boolean, promise: Promise ->
val manager = appContext.permissions ?: throw Exceptions.PermissionsModuleNotFound()
val permissions = getMediaLibraryPermissions(writeOnly)
manager.askForPermissions(createPermissionsDecorator(promise), *permissions)
}
AsyncFunction("getMediaLibraryPermissionsAsync") { writeOnly: Boolean, promise: Promise ->
val manager = appContext.permissions ?: throw Exceptions.PermissionsModuleNotFound()
val permissions = getMediaLibraryPermissions(writeOnly)
manager.getPermissions(createPermissionsDecorator(promise), *permissions)
}
AsyncFunction("requestCameraPermissionsAsync") { promise: Promise ->
Permissions.askForPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.CAMERA)
}
AsyncFunction("getCameraPermissionsAsync") { promise: Promise ->
Permissions.getPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.CAMERA)
}
AsyncFunction("launchCameraAsync") Coroutine { options: ImagePickerOptions ->
ensureTargetActivityIsAvailable(options)
ensureCameraPermissionsAreGranted()
val mediaFile = createOutputFile(cacheDirectory, options.mediaTypes.toFileExtension())
val uri = mediaFile.toContentUri(context)
val contractOptions = options.toCameraContractOptions(uri.toString())
launchContract({ cameraLauncher.launch(contractOptions) }, options)
}
AsyncFunction("launchImageLibraryAsync") Coroutine { options: ImagePickerOptions ->
val contractOptions = options.toImageLibraryContractOptions()
launchContract({ imageLibraryLauncher.launch(contractOptions) }, options)
}
AsyncFunction("getPendingResultAsync") Coroutine { ->
val (bareResult, options) = pendingMediaPickingResult ?: return@Coroutine null
pendingMediaPickingResult = null
mediaHandler.readExtras(bareResult, options)
}
// endregion
RegisterActivityContracts {
cameraLauncher = registerForActivityResult(
CameraContract(this@ImagePickerModule)
) { input, result -> handleResultUponActivityDestruction(result, input.options) }
imageLibraryLauncher = registerForActivityResult(
ImageLibraryContract(this@ImagePickerModule)
) { input, result -> handleResultUponActivityDestruction(result, input.options) }
cropImageLauncher = registerForActivityResult(
CropImageContract(this@ImagePickerModule)
) { input, result -> handleResultUponActivityDestruction(result, input.options) }
}
}
// TODO (@bbarthec): generalize it as almost every module re-declares this approach
val context: Context
get() = requireNotNull(appContext.reactContext) { "React Application Context is null" }
private val currentActivity
get() = appContext.activityProvider?.currentActivity ?: throw MissingCurrentActivityException()
private val mediaHandler = MediaHandler(this)
private lateinit var cameraLauncher: AppContextActivityResultLauncher<CameraContractOptions, ImagePickerContractResult>
private lateinit var imageLibraryLauncher: AppContextActivityResultLauncher<ImageLibraryContractOptions, ImagePickerContractResult>
private lateinit var cropImageLauncher: AppContextActivityResultLauncher<CropImageContractOptions, ImagePickerContractResult>
private val cacheDirectory: File
get() = appContext.cacheDirectory
/**
* Stores result for an operation that has been interrupted by the activity destruction.
* The results are stored only for successful, non-cancelled-by-user scenario.
* Each new picking operation overrides previous state (for cancelled operation `null` is set).
* The user can retrieve the data using exported `getPendingResultAsync` method.
*/
private var pendingMediaPickingResult: PendingMediaPickingResult? = null
private var isPickerOpen = false
private fun createPermissionsDecorator(promise: Promise): PermissionsResponseListener {
val weakContext = appContext.reactContext.weak()
return PermissionsResponseListener { permissionsMap ->
val areAllGranted = permissionsMap.all { (_, response) -> response.status == PermissionsStatus.GRANTED }
val areAllDenied = permissionsMap.isNotEmpty() && permissionsMap.all { (_, response) -> response.status == PermissionsStatus.DENIED }
val canAskAgain = permissionsMap.all { (_, response) -> response.canAskAgain }
val permissionsBundle =
Bundle().apply {
putString(PermissionsResponse.EXPIRES_KEY, PermissionsResponse.PERMISSION_EXPIRES_NEVER)
putString(
PermissionsResponse.STATUS_KEY,
when {
areAllGranted -> PermissionsStatus.GRANTED.status
areAllDenied -> PermissionsStatus.DENIED.status
else -> PermissionsStatus.UNDETERMINED.status
}
)
putBoolean(PermissionsResponse.CAN_ASK_AGAIN_KEY, canAskAgain)
putBoolean(PermissionsResponse.GRANTED_KEY, areAllGranted)
}
if (areAllGranted) {
permissionsBundle.putString(ACCESS_PRIVILEGES_PERMISSION_KEY, "all")
promise.resolve(permissionsBundle)
return@PermissionsResponseListener
}
// On Android < 14 we always return `all` or `none`, since it doesn't support limited access
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
permissionsBundle.putString(ACCESS_PRIVILEGES_PERMISSION_KEY, "none")
promise.resolve(permissionsBundle)
return@PermissionsResponseListener
}
val context = weakContext.get() ?: run {
promise.reject(Exceptions.ReactContextLost())
return@PermissionsResponseListener
}
// For photo and video access android will return DENIED status if the user selected "allow only selected"
// We need to check if that is the case and overwrite the result.
val hasPartialAccess = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED
if (hasPartialAccess) {
permissionsBundle.putBoolean(PermissionsResponse.GRANTED_KEY, true)
permissionsBundle.putBoolean(PermissionsResponse.CAN_ASK_AGAIN_KEY, true)
permissionsBundle.putString(PermissionsResponse.STATUS_KEY, PermissionsStatus.GRANTED.status)
permissionsBundle.putString(ACCESS_PRIVILEGES_PERMISSION_KEY, "limited")
} else {
permissionsBundle.putString(ACCESS_PRIVILEGES_PERMISSION_KEY, "none")
}
promise.resolve(permissionsBundle)
}
}
/**
* Calls [launchPicker] and unifies flow shared between "launchCameraAsync" and "launchImageLibraryAsync"
*/
private suspend fun launchContract(
pickerLauncher: suspend () -> ImagePickerContractResult,
options: ImagePickerOptions
): Any {
return try {
if (isPickerOpen) {
return ImagePickerResponse(canceled = true)
}
isPickerOpen = true
var result = launchPicker(pickerLauncher)
if (
!options.allowsMultipleSelection &&
options.allowsEditing &&
result.data.size == 1 &&
result.data[0].first == MediaType.IMAGE
) {
result = launchPicker {
cropImageLauncher.launch(CropImageContractOptions(result.data[0].second.toString(), options))
}
}
mediaHandler.readExtras(result.data, options)
} catch (cause: OperationCanceledException) {
return ImagePickerResponse(canceled = true)
} finally {
isPickerOpen = false
}
}
/**
* Function that would store the results coming from 3-rd party Activity in case Android decides to
* destroy the launching application that is backgrounded.
*/
private fun handleResultUponActivityDestruction(result: ImagePickerContractResult, options: ImagePickerOptions) {
if (result is ImagePickerContractResult.Success) {
pendingMediaPickingResult = PendingMediaPickingResult(result.data, options)
}
}
/**
* Launches picker (image library or camera)
*/
private suspend fun launchPicker(
pickerLauncher: suspend () -> ImagePickerContractResult
): ImagePickerContractResult.Success = withContext(Dispatchers.IO) {
when (val pickingResult = pickerLauncher()) {
is ImagePickerContractResult.Success -> pickingResult
is ImagePickerContractResult.Cancelled -> throw OperationCanceledException()
is ImagePickerContractResult.Error -> throw FailedToPickMediaException()
}
}
// endregion
// region Utils
private fun getMediaLibraryPermissions(writeOnly: Boolean): Array<String> =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOfNotNull(
READ_MEDIA_IMAGES,
READ_MEDIA_VIDEO
).toTypedArray()
} else {
listOfNotNull(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE.takeIf { !writeOnly }
).toTypedArray()
}
private fun ensureTargetActivityIsAvailable(options: ImagePickerOptions) {
val cameraIntent = Intent(options.mediaTypes.toCameraIntentAction())
if (cameraIntent.resolveActivity(currentActivity.application.packageManager) == null) {
throw MissingActivityToHandleIntent(cameraIntent.type)
}
}
private suspend fun ensureCameraPermissionsAreGranted(): Unit = suspendCancellableCoroutine { continuation ->
val permissions = appContext.permissions ?: throw ModuleNotFoundException("Permissions")
permissions.askForPermissions(
{ permissionsResponse ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (permissionsResponse[Manifest.permission.CAMERA]?.status == PermissionsStatus.GRANTED) {
continuation.resume(Unit)
} else {
continuation.resumeWithException(UserRejectedPermissionsException())
}
} else if (
permissionsResponse[Manifest.permission.WRITE_EXTERNAL_STORAGE]?.status == PermissionsStatus.GRANTED &&
permissionsResponse[Manifest.permission.CAMERA]?.status == PermissionsStatus.GRANTED
) {
continuation.resume(Unit)
} else {
continuation.resumeWithException(UserRejectedPermissionsException())
}
},
*listOfNotNull(
Manifest.permission.WRITE_EXTERNAL_STORAGE.takeIf { Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU },
Manifest.permission.CAMERA
).toTypedArray()
)
}
// endregion
}
/**
* Simple data structure to hold the data that has to be preserved after the Activity is destroyed.
*/
internal data class PendingMediaPickingResult(
val data: List<Pair<MediaType, Uri>>,
val options: ImagePickerOptions
)

View File

@@ -0,0 +1,96 @@
package expo.modules.imagepicker
import java.io.Serializable
import android.provider.MediaStore
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
import expo.modules.imagepicker.contracts.CameraContractOptions
import expo.modules.imagepicker.contracts.ImageLibraryContractOptions
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
import expo.modules.kotlin.types.Enumerable
internal const val UNLIMITED_SELECTION: Int = 0
internal class ImagePickerOptions : Record, Serializable {
@Field
var allowsEditing: Boolean = false
@Field
var allowsMultipleSelection: Boolean = false
@Field
@FloatRange(from = 0.0, to = 1.0)
var quality: Double = 0.2
@Field
@IntRange(from = 0)
var selectionLimit: Int = UNLIMITED_SELECTION
@Field
var base64: Boolean = false
@Field
var exif: Boolean = false
@Field
var mediaTypes: MediaTypes = MediaTypes.IMAGES
@IntRange(from = 0)
var videoMaxDuration: Int = 0
@Field
var aspect: Pair<Int, Int>? = null
@Field
var cameraType: CameraType = CameraType.BACK
@Field
val legacy: Boolean = false
fun toCameraContractOptions(uri: String) = CameraContractOptions(uri, this)
fun toImageLibraryContractOptions() = ImageLibraryContractOptions(this)
}
internal enum class MediaTypes(val value: String) : Enumerable {
IMAGES("Images"),
VIDEOS("Videos"),
ALL("All");
fun toMimeType(): String {
return when (this) {
IMAGES -> ImageAllMimeType
VIDEOS -> VideoAllMimeType
ALL -> AllMimeType
}
}
fun toFileExtension(): String {
return when (this) {
VIDEOS -> ".mp4"
else -> ".jpeg"
}
}
/**
* Return [MediaStore]'s intent capture action associated with given media types
*/
fun toCameraIntentAction(): String {
return when (this) {
VIDEOS -> MediaStore.ACTION_VIDEO_CAPTURE
else -> MediaStore.ACTION_IMAGE_CAPTURE
}
}
private companion object {
const val ImageAllMimeType = "image/*"
const val VideoAllMimeType = "video/*"
const val AllMimeType = "*/*"
}
}
internal enum class CameraType(val value: String) : Enumerable {
BACK("back"),
FRONT("front")
}

View File

@@ -0,0 +1,31 @@
package expo.modules.imagepicker
import android.os.Bundle
import expo.modules.kotlin.records.Field
import expo.modules.kotlin.records.Record
import expo.modules.kotlin.types.Enumerable
internal class ImagePickerAsset(
@Field val assetId: String? = null,
@Field val type: MediaType = MediaType.IMAGE,
@Field val uri: String = "",
@Field val width: Int = 0,
@Field val height: Int = 0,
@Field val fileName: String? = null,
@Field val fileSize: Long? = null,
@Field val mimeType: String? = null,
@Field val base64: String? = null,
@Field val exif: Bundle? = null,
@Field val duration: Int? = null,
@Field val rotation: Int? = null
) : Record
internal class ImagePickerResponse(
@Field val canceled: Boolean = false,
@Field val assets: List<ImagePickerAsset>? = null
) : Record
enum class MediaType(val value: String) : Enumerable {
VIDEO("video"),
IMAGE("image")
}

View File

@@ -0,0 +1,280 @@
package expo.modules.imagepicker
import android.content.ClipData
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.provider.DocumentsContract
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import androidx.exifinterface.media.ExifInterface
import expo.modules.core.utilities.FileUtilities
import expo.modules.imagepicker.ImagePickerConstants.TAG
import kotlinx.coroutines.runInterruptible
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
internal fun createOutputFile(cacheDir: File, extension: String): File {
val filePath = FileUtilities.generateOutputPath(cacheDir, ImagePickerConstants.CACHE_DIR_NAME, extension)
return try {
File(filePath).apply { createNewFile() }
} catch (cause: IOException) {
throw FailedToCreateFileException(filePath, cause)
}
}
internal fun getType(contentResolver: ContentResolver, uri: Uri): String =
contentResolver.getType(uri)
?: getTypeFromFileUrl(uri.toString())
?: throw FailedToDeduceTypeException()
private fun getTypeFromFileUrl(url: String): String? {
val extension = MimeTypeMap.getFileExtensionFromUrl(url)
return extension?.let { MimeTypeMap.getSingleton().getMimeTypeFromExtension(it) }
}
/**
* Convert this [File] to [Uri] that might be accessed by 3rd party Activities, eg. by camera application
*/
internal fun File.toContentUri(context: Context): Uri {
return try {
FileProvider.getUriForFile(context, context.packageName + ".ImagePickerFileProvider", this)
} catch (e: Exception) {
Uri.fromFile(this)
}
}
internal fun File.toBitmapCompressFormat(): Bitmap.CompressFormat = when {
this.extension.endsWith("png", ignoreCase = true) -> Bitmap.CompressFormat.PNG
else -> Bitmap.CompressFormat.JPEG
}
internal fun Bitmap.CompressFormat.toImageFileExtension(): String {
return when (this) {
Bitmap.CompressFormat.PNG -> ".png"
Bitmap.CompressFormat.JPEG -> ".jpeg"
else -> throw RuntimeException("Compress format not supported '${this.name}'")
}
}
internal fun String.toImageFileExtension(): String = when {
this.endsWith("png", ignoreCase = true) -> ".png"
this.endsWith("gif", ignoreCase = true) -> ".gif"
this.endsWith("bmp", ignoreCase = true) -> ".bmp"
this.endsWith("webp", ignoreCase = true) -> ".webp"
!this.endsWith("jpeg", ignoreCase = true) -> {
Log.w(TAG, "Image file $this is of unsupported type. Falling back to JPEG instead.")
".jpeg"
}
else -> ".jpeg"
}
internal fun Uri.toMediaType(contentResolver: ContentResolver): MediaType {
val type = getType(contentResolver, this)
return when {
type.contains("image/") -> MediaType.IMAGE
type.contains("video/") -> MediaType.VIDEO
else -> throw FailedToDeduceTypeException()
}
}
internal fun String.toBitmapCompressFormat(): Bitmap.CompressFormat = when {
this.endsWith("png", ignoreCase = true) ||
this.endsWith("gif", ignoreCase = true) ||
this.endsWith("bmp", ignoreCase = true) ||
this.endsWith("webp", ignoreCase = true) -> {
// The result image won't ever be a GIF of a BMP as the cropper doesn't support it.
Bitmap.CompressFormat.PNG
}
else -> {
if (!this.endsWith("jpeg", ignoreCase = true)) {
Log.w(TAG, "Image file $this is of unsupported type. Falling back to JPEG instead.")
}
Bitmap.CompressFormat.JPEG
}
}
internal fun MediaMetadataRetriever.extractInt(key: Int): Int {
return this.extractMetadata(key)?.toInt() ?: throw FailedToExtractVideoMetadataException()
}
/**
* [Iterable] implementation for [ClipData] items
*/
val ClipData.items: Iterable<ClipData.Item>
get() = object : Iterable<ClipData.Item> {
override fun iterator() = object : Iterator<ClipData.Item> {
var index = 0
val count = itemCount
override fun hasNext(): Boolean = index < count
override fun next(): ClipData.Item = getItemAt(index++)
}
}
/**
* Gets all data that is associated with this [Intent].
* Original data order is preserved.
*
* Adapted from [androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents.getClipDataUris]
*/
internal fun Intent.getAllDataUris(): List<Uri> {
// Use a LinkedHashSet to maintain any ordering that may be present in the ClipData
val resultSet = LinkedHashSet<Uri>()
data
?.let { resultSet.add(it) }
clipData
?.items
?.map { it.uri }
?.let { resultSet.addAll(it) }
return resultSet.toList()
}
/**
* Copy the media file from `sourceUri` to `destinationUri`.
*
* @param sourceUri uri to the file to copy the data from
* @param targetFile file to save the media data into
*/
internal suspend fun copyFile(
sourceUri: Uri,
targetFile: File,
contentResolver: ContentResolver
) = runInterruptible {
val targetUri = Uri.fromFile(targetFile)
// source and destination are the same file
if (sourceUri.compareTo(targetUri) == 0) {
return@runInterruptible
}
try {
contentResolver.openInputStream(sourceUri)?.use { inputStream ->
FileOutputStream(targetFile).use { fileOutputStream ->
inputStream.copyTo(fileOutputStream)
return@runInterruptible
}
} ?: throw FailedToReadFileException(sourceUri.toFile())
} catch (cause: FileNotFoundException) {
throw FailedToWriteFileException(targetFile, cause)
}
}
internal suspend fun copyExifData(
sourceUri: Uri,
targetFile: File,
contentResolver: ContentResolver
) = runInterruptible {
val targetUri = Uri.fromFile(targetFile)
if (sourceUri.compareTo(targetUri) == 0) {
return@runInterruptible
}
val omittableTags = listOf(
ExifInterface.TAG_IMAGE_LENGTH,
ExifInterface.TAG_IMAGE_WIDTH,
ExifInterface.TAG_PIXEL_X_DIMENSION,
ExifInterface.TAG_PIXEL_Y_DIMENSION,
ExifInterface.TAG_ORIENTATION
)
try {
contentResolver.openInputStream(sourceUri)?.use { inputStream ->
val sourceExif = ExifInterface(inputStream)
val targetExif = ExifInterface(targetFile)
ImagePickerConstants.EXIF_TAGS
.filter { (_, tag) -> !omittableTags.contains(tag) }
.map { (_, tag) -> tag to sourceExif.getAttribute(tag) }
.filter { (_, value) -> value != null }
.forEach { (tag, value) -> targetExif.setAttribute(tag, value) }
try {
targetExif.saveAttributes()
} catch (cause: IOException) {
throw FailedToWriteExifDataToFileException(targetFile, cause)
}
} ?: throw FailedToReadFileException(sourceUri.toFile())
} catch (cause: FileNotFoundException) {
throw FailedToWriteFileException(targetFile, cause)
}
}
/*
Getting asset ID (and metadata) on Android is not that obvious. When getting a `content://` URI
using `ACTION_GET_CONTENT` or `ACTION_OPEN_DOCUMENT` intents, there are 3 possible ways:
1. When the user selects a photo from **Images** section of the picker (on the left drawer)
In this case we get a URI from `com.android.providers.media.MediaDocumentsProvider`,
that inherits from `DocumentsProvider`. The URI looks like this:
```
com.android.providers.media.documents/document/image:56
```
In this case, the `56` is the ID we're looking for.
2. When the user selects a photo from **Downloads** section, another content provider is used:
`DownloadStorageProvider` which is a bit different, and also differs depending on Android version:
- On API 29+ it also inherits from `com.android.providers.downloads.DocumentsProvider`
and the URI looks like this:
```
com.android.providers.downloads.documents/document/msf:56
```
Where "msf" is abbr. of "media store file" and 56 is our asset ID
- On API <29 it looks similar:
```
com.android.providers.downloads.documents/document/128
```
but the 128 is an internal ID of downloads provider, unrelated to media store asset ID.
3. When the user selects a photo by browsing the filesystem, the URI looks like this:
```
com.android.externalstorage.documents/document/primary:Download:filename.jpg
```
No ID in this case
*/
/**
* Checks whether this [Uri] is a `com.android.providers.media.documents` provider uri
*/
internal val Uri.isMediaProviderUri
get() = this.authority == "com.android.providers.media.documents"
/**
* Checks whether this [Uri] is a `com.android.providers.downloads.documents` provider uri
*/
internal val Uri.isDownloadsProviderUri
get() = this.authority == "com.android.providers.downloads.documents"
/**
* Checks whether asset represented by this [Uri] can be queried in the media store
*/
internal val Uri.isMediaStoreAssetUri
get() = isMediaProviderUri || (
isDownloadsProviderUri &&
DocumentsContract
.getDocumentId(this)
.startsWith("msf:")
)
/**
* If the URI represents a media store asset, this returns its ID. Otherwise, returns `null`.
*
* See the detailed explanation above in this file (ImagePickerUtils.kt).
*/
internal fun Uri.getMediaStoreAssetId(): String? {
if (isMediaStoreAssetUri) {
val rawId = DocumentsContract.getDocumentId(this)
return if (rawId.contains(':')) rawId.split(':')[1] else rawId
}
return null
}

View File

@@ -0,0 +1,125 @@
package expo.modules.imagepicker
import android.content.Context
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Base64
import androidx.core.net.toUri
import expo.modules.imagepicker.exporters.CompressionImageExporter
import expo.modules.imagepicker.exporters.ImageExporter
import expo.modules.imagepicker.exporters.RawImageExporter
import expo.modules.kotlin.providers.AppContextProvider
import java.io.File
internal class MediaHandler(
private val appContextProvider: AppContextProvider
) {
private val context: Context
get() = requireNotNull(appContextProvider.appContext.reactContext) { "React Application Context is null" }
internal suspend fun readExtras(
bareResult: List<Pair<MediaType, Uri>>,
options: ImagePickerOptions
): ImagePickerResponse {
val results = bareResult.map { (mediaType, uri) ->
when (mediaType) {
MediaType.VIDEO -> handleVideo(uri)
MediaType.IMAGE -> handleImage(uri, options)
}
}
return ImagePickerResponse(
canceled = false,
assets = results
)
}
private val cacheDirectory: File
get() = appContextProvider.appContext.cacheDirectory
private suspend fun handleImage(
sourceUri: Uri,
options: ImagePickerOptions
): ImagePickerAsset {
val exporter: ImageExporter = if (options.quality == ImagePickerConstants.MAXIMUM_QUALITY) {
RawImageExporter()
} else {
CompressionImageExporter(appContextProvider, options.quality)
}
val mimeType = getType(context.contentResolver, sourceUri)
val outputFile = createOutputFile(cacheDirectory, mimeType.toImageFileExtension())
val exportedImage = exporter.exportAsync(sourceUri, outputFile, context.contentResolver)
val base64 = options.base64.takeIf { it }
?.let { exportedImage.data(context.contentResolver) }
?.let { Base64.encodeToString(it.toByteArray(), Base64.NO_WRAP) }
val exif = options.exif.takeIf { it }
?.let { exportedImage.exif(context.contentResolver) }
val fileData = getAdditionalFileData(sourceUri)
return ImagePickerAsset(
type = MediaType.IMAGE,
uri = Uri.fromFile(outputFile).toString(),
width = exportedImage.width,
height = exportedImage.height,
fileName = fileData?.fileName ?: outputFile.name,
fileSize = fileData?.fileSize ?: outputFile.length(),
mimeType = mimeType,
base64 = base64,
exif = exif,
assetId = sourceUri.getMediaStoreAssetId()
)
}
private fun getAdditionalFileData(uri: Uri): AdditionalFileData? = context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
val name: String? = cursor.getString(nameIndex)
val size = cursor.getLong(sizeIndex)
AdditionalFileData(
name,
size
)
}
private suspend fun handleVideo(
sourceUri: Uri
): ImagePickerAsset {
val outputFile = createOutputFile(cacheDirectory, ".mp4")
copyFile(sourceUri, outputFile, context.contentResolver)
val outputUri = outputFile.toUri()
try {
val metadataRetriever = MediaMetadataRetriever().apply {
setDataSource(context, outputUri)
}
val fileData = getAdditionalFileData(sourceUri)
val mimeType = getType(context.contentResolver, sourceUri)
return ImagePickerAsset(
type = MediaType.VIDEO,
uri = outputUri.toString(),
width = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH),
height = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT),
fileName = fileData?.fileName,
fileSize = fileData?.fileSize,
mimeType = mimeType,
duration = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_DURATION),
rotation = metadataRetriever.extractInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION),
assetId = sourceUri.getMediaStoreAssetId()
)
} catch (cause: FailedToExtractVideoMetadataException) {
throw FailedToExtractVideoMetadataException(outputFile, cause)
}
}
}
data class AdditionalFileData(
val fileName: String?,
val fileSize: Long?
)

View File

@@ -0,0 +1,66 @@
package expo.modules.imagepicker.contracts
import android.app.Activity
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.net.toUri
import expo.modules.imagepicker.ImagePickerOptions
import expo.modules.imagepicker.CameraType
import expo.modules.imagepicker.toMediaType
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
import expo.modules.kotlin.providers.AppContextProvider
import java.io.Serializable
/**
* An [ActivityResultContract] to [take a picture][MediaStore.ACTION_IMAGE_CAPTURE] or [take a video][MediaStore.ACTION_VIDEO_CAPTURE]
* saving it into the provided content-[Uri].
*
* @see [androidx.activity.result.contract.ActivityResultContracts.TakePicture] or [androidx.activity.result.contract.ActivityResultContracts.CaptureVideo]
*/
internal class CameraContract(
private val appContextProvider: AppContextProvider
) : AppContextActivityResultContract<CameraContractOptions, ImagePickerContractResult> {
private val contentResolver: ContentResolver
get() = requireNotNull(appContextProvider.appContext.reactContext) {
"React Application Context is null"
}.contentResolver
override fun createIntent(context: Context, input: CameraContractOptions): Intent =
Intent(input.options.mediaTypes.toCameraIntentAction())
.putExtra(MediaStore.EXTRA_OUTPUT, input.uri.toUri())
.apply {
if (input.options.mediaTypes.toCameraIntentAction() == MediaStore.ACTION_VIDEO_CAPTURE) {
putExtra(MediaStore.EXTRA_DURATION_LIMIT, input.options.videoMaxDuration)
}
if (input.options.cameraType == CameraType.FRONT) {
putExtra("android.intent.extras.LENS_FACING_FRONT", 1)
putExtra("android.intent.extras.CAMERA_FACING", 1)
putExtra("android.intent.extra.USE_FRONT_CAMERA", true)
} else {
putExtra("android.intent.extras.LENS_FACING_BACK", 1)
putExtra("android.intent.extras.CAMERA_FACING", 0)
putExtra("android.intent.extra.USE_FRONT_CAMERA", false)
}
}
override fun parseResult(input: CameraContractOptions, resultCode: Int, intent: Intent?): ImagePickerContractResult =
if (resultCode == Activity.RESULT_CANCELED) {
ImagePickerContractResult.Cancelled
} else {
val uri = Uri.parse(input.uri)
val type = uri.toMediaType(contentResolver)
ImagePickerContractResult.Success(listOf(type to uri))
}
}
internal data class CameraContractOptions(
/**
* Destination file in a form of content-[Uri] to save results coming from camera to.
*/
val uri: String,
val options: ImagePickerOptions
) : Serializable

View File

@@ -0,0 +1,13 @@
package expo.modules.imagepicker.contracts
import android.net.Uri
import expo.modules.imagepicker.MediaType
/**
* Data required to be returned upon successful contract completion
*/
internal sealed class ImagePickerContractResult private constructor() {
class Success(val data: List<Pair<MediaType, Uri>>) : ImagePickerContractResult()
object Cancelled : ImagePickerContractResult()
object Error : ImagePickerContractResult()
}

View File

@@ -0,0 +1,74 @@
package expo.modules.imagepicker.contracts
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageActivity
import com.canhub.cropper.CropImageOptions
import expo.modules.imagepicker.ImagePickerOptions
import expo.modules.imagepicker.MediaType
import expo.modules.imagepicker.copyExifData
import expo.modules.imagepicker.createOutputFile
import expo.modules.imagepicker.toBitmapCompressFormat
import expo.modules.imagepicker.toImageFileExtension
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
import expo.modules.kotlin.providers.AppContextProvider
import kotlinx.coroutines.runBlocking
import java.io.Serializable
internal class CropImageContract(
private val appContextProvider: AppContextProvider
) : AppContextActivityResultContract<CropImageContractOptions, ImagePickerContractResult> {
override fun createIntent(context: Context, input: CropImageContractOptions) = Intent(context, CropImageActivity::class.java).apply {
val mediaType = expo.modules.imagepicker.getType(context.contentResolver, input.sourceUri.toUri())
val compressFormat = mediaType.toBitmapCompressFormat()
val cacheDirectory = appContextProvider.appContext.cacheDirectory
val outputUri = createOutputFile(cacheDirectory, compressFormat.toImageFileExtension()).toUri()
putExtra(
CropImage.CROP_IMAGE_EXTRA_BUNDLE,
bundleOf(
CropImage.CROP_IMAGE_EXTRA_SOURCE to input.sourceUri.toUri(),
CropImage.CROP_IMAGE_EXTRA_OPTIONS to CropImageOptions().apply {
outputCompressFormat = compressFormat
outputCompressQuality = (input.options.quality * 100).toInt()
this.customOutputUri = outputUri
input.options.aspect?.let { (x, y) ->
aspectRatioX = x
aspectRatioY = y
fixAspectRatio = true
initialCropWindowPaddingRatio = 0f
}
}
)
)
}
override fun parseResult(input: CropImageContractOptions, resultCode: Int, intent: Intent?): ImagePickerContractResult {
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent?.getParcelableExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, CropImage.ActivityResult::class.java)
} else {
@Suppress("DEPRECATION")
intent?.getParcelableExtra(CropImage.CROP_IMAGE_EXTRA_RESULT)
}
if (resultCode == Activity.RESULT_CANCELED || result == null) {
return ImagePickerContractResult.Cancelled
}
val targetUri = requireNotNull(result.uriContent)
val contentResolver = requireNotNull(appContextProvider.appContext.reactContext) { "React Application Context is null" }.contentResolver
runBlocking { copyExifData(input.sourceUri.toUri(), targetUri.toFile(), contentResolver) }
return ImagePickerContractResult.Success(listOf(MediaType.IMAGE to targetUri))
}
}
internal data class CropImageContractOptions(
val sourceUri: String,
val options: ImagePickerOptions
) : Serializable

View File

@@ -0,0 +1,132 @@
package expo.modules.imagepicker.contracts
import android.app.Activity
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import expo.modules.imagepicker.ImagePickerOptions
import expo.modules.imagepicker.MediaTypes
import expo.modules.imagepicker.UNLIMITED_SELECTION
import expo.modules.imagepicker.getAllDataUris
import expo.modules.imagepicker.toMediaType
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.providers.AppContextProvider
import java.io.Serializable
/**
* An [androidx.activity.result.contract.ActivityResultContract] to prompt the user to pick single or multiple image(s) or/and video(s),
* receiving a `content://` [Uri] for each piece of content.
*
* @see [androidx.activity.result.contract.ActivityResultContracts.GetContent],
* @see [androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents]
*/
internal class ImageLibraryContract(
private val appContextProvider: AppContextProvider
) : AppContextActivityResultContract<ImageLibraryContractOptions, ImagePickerContractResult> {
private val contentResolver: ContentResolver
get() = appContextProvider.appContext.reactContext?.contentResolver
?: throw Exceptions.ReactContextLost()
override fun createIntent(context: Context, input: ImageLibraryContractOptions): Intent {
if (input.options.legacy) {
return createLegacyIntent(input.options)
}
val request = PickVisualMediaRequest.Builder()
.setMediaType(
when (input.options.mediaTypes) {
MediaTypes.VIDEOS -> {
PickVisualMedia.VideoOnly
}
MediaTypes.IMAGES -> {
PickVisualMedia.ImageOnly
}
else -> {
PickVisualMedia.ImageAndVideo
}
}
)
.build()
if (input.options.allowsMultipleSelection) {
val selectionLimit = input.options.selectionLimit
if (selectionLimit == 1) {
// If multiple selection is allowed but the limit is 1, we should ignore
// the multiple selection flag and just treat it as a single selection.
return PickVisualMedia().createIntent(context, request)
}
if (selectionLimit > 1) {
return PickMultipleVisualMedia(selectionLimit).createIntent(context, request)
}
// If the selection limit is 0, it is the same as unlimited selection.
if (selectionLimit == UNLIMITED_SELECTION) {
return PickMultipleVisualMedia().createIntent(context, request)
}
}
return PickVisualMedia().createIntent(context, request)
}
override fun parseResult(input: ImageLibraryContractOptions, resultCode: Int, intent: Intent?) =
if (resultCode == Activity.RESULT_CANCELED) {
ImagePickerContractResult.Cancelled
} else {
intent?.takeIf { resultCode == Activity.RESULT_OK }?.getAllDataUris()?.let { uris ->
if (input.options.allowsMultipleSelection) {
val results = uris.map { uri ->
uri.toMediaType(contentResolver) to uri
}.let {
if (input.options.selectionLimit > 0) {
it.take(input.options.selectionLimit)
} else {
it
}
}
ImagePickerContractResult.Success(results)
} else {
if (intent.data != null) {
intent.data?.let { uri ->
val type = uri.toMediaType(contentResolver)
ImagePickerContractResult.Success(listOf(type to uri))
}
} else {
uris.firstOrNull()?.let { uri ->
val type = uri.toMediaType(contentResolver)
ImagePickerContractResult.Success(listOf(type to uri))
} ?: ImagePickerContractResult.Error
}
}
} ?: ImagePickerContractResult.Error
}
private fun createLegacyIntent(options: ImagePickerOptions) = Intent(Intent.ACTION_GET_CONTENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("*/*")
.putExtra(
Intent.EXTRA_MIME_TYPES,
when (options.mediaTypes) {
MediaTypes.IMAGES -> arrayOf("image/*")
MediaTypes.VIDEOS -> arrayOf("video/*")
else -> arrayOf("image/*", "video/*")
}
).apply {
if (options.allowsMultipleSelection) {
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
}
}
internal data class ImageLibraryContractOptions(
val options: ImagePickerOptions
) : Serializable

View File

@@ -0,0 +1,79 @@
package expo.modules.imagepicker.exporters
import android.content.ContentResolver
import android.graphics.Bitmap
import android.net.Uri
import androidx.annotation.FloatRange
import androidx.core.net.toFile
import expo.modules.imagepicker.FailedToReadFileException
import expo.modules.imagepicker.FailedToWriteFileException
import expo.modules.imagepicker.MissingModuleException
import expo.modules.imagepicker.copyExifData
import expo.modules.imagepicker.toBitmapCompressFormat
import expo.modules.kotlin.providers.AppContextProvider
import kotlinx.coroutines.runInterruptible
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.ExecutionException
class CompressionImageExporter(
private val appContextProvider: AppContextProvider,
@FloatRange(from = 0.0, to = 1.0)
quality: Double
) : ImageExporter {
private val compressQuality = (quality * 100).toInt()
override suspend fun exportAsync(
source: Uri,
output: File,
contentResolver: ContentResolver
): ImageExportResult {
val bitmap = readBitmap(source)
val compressFormat = output.toBitmapCompressFormat()
writeImage(bitmap, output, compressFormat)
copyExifData(source, output, contentResolver)
return object : ImageExportResult(
bitmap.width,
bitmap.height,
output
) {
override suspend fun data(contentResolver: ContentResolver): ByteArrayOutputStream {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, compressQuality, outputStream)
return outputStream
}
}
}
private suspend fun readBitmap(source: Uri): Bitmap = runInterruptible {
val loaderResult = appContextProvider.appContext.imageLoader
?.loadImageForManipulationFromURL(source.toString())
?: throw MissingModuleException("ImageLoader")
try {
loaderResult.get()
} catch (cause: ExecutionException) {
throw FailedToReadFileException(source.toFile(), cause)
}
}
/**
* Compress and save the `bitmap` to `file`
* @throws [IOException]
*/
private suspend fun writeImage(
bitmap: Bitmap,
output: File,
compressFormat: Bitmap.CompressFormat
) = runInterruptible {
try {
FileOutputStream(output).use { out -> bitmap.compress(compressFormat, compressQuality, out) }
} catch (cause: FileNotFoundException) {
throw FailedToWriteFileException(output, cause)
}
}
}

View File

@@ -0,0 +1,71 @@
package expo.modules.imagepicker.exporters
import android.content.ContentResolver
import android.net.Uri
import android.os.Bundle
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface
import expo.modules.imagepicker.FailedToReadFileException
import expo.modules.imagepicker.ImagePickerConstants
import kotlinx.coroutines.runInterruptible
import java.io.ByteArrayOutputStream
import java.io.File
/**
* Interface allowing exporting an image in a different ways.
*/
interface ImageExporter {
/**
* Export the file under `source` [Uri] to the `output` [File]
*/
suspend fun exportAsync(source: Uri, output: File, contentResolver: ContentResolver): ImageExportResult
}
/**
* Results of exporting an image to the given file.
* Allows accessing extra data associated with the underlying image file.
*/
open class ImageExportResult(
val width: Int,
val height: Int,
private val imageFile: File
) {
/**
* Allows accessing the underlying byte data in a lazy manner.
*/
open suspend fun data(contentResolver: ContentResolver): ByteArrayOutputStream = runInterruptible {
contentResolver.openInputStream(imageFile.toUri())?.use { inputStream ->
ByteArrayOutputStream().use { outputStream ->
inputStream.copyTo(outputStream)
return@runInterruptible outputStream
}
} ?: throw FailedToReadFileException(imageFile)
}
/**
* Allows accessing to the EXIF data associated with this image.
*/
open suspend fun exif(contentResolver: ContentResolver): Bundle = runInterruptible {
contentResolver.openInputStream(imageFile.toUri())?.use { inputStream ->
return@runInterruptible Bundle().apply {
val exifInterface = ExifInterface(inputStream)
ImagePickerConstants.EXIF_TAGS
.filter { (_, tag) -> exifInterface.getAttribute(tag) != null }
.forEach { (type, tag) ->
when (type) {
"string" -> putString(tag, exifInterface.getAttribute(tag))
"int" -> putInt(tag, exifInterface.getAttributeInt(tag, 0))
"double" -> putDouble(tag, exifInterface.getAttributeDouble(tag, 0.0))
}
}
// Explicitly get latitude, longitude, altitude with their specific accessor functions.
exifInterface.latLong?.let { latLong ->
putDouble(ExifInterface.TAG_GPS_LATITUDE, latLong[0])
putDouble(ExifInterface.TAG_GPS_LONGITUDE, latLong[1])
putDouble(ExifInterface.TAG_GPS_ALTITUDE, exifInterface.getAltitude(0.0))
}
}
} ?: throw FailedToReadFileException(imageFile)
}
}

View File

@@ -0,0 +1,35 @@
package expo.modules.imagepicker.exporters
import android.content.ContentResolver
import android.graphics.BitmapFactory
import android.media.ExifInterface
import android.net.Uri
import expo.modules.imagepicker.copyFile
import java.io.File
class RawImageExporter : ImageExporter {
override suspend fun exportAsync(
source: Uri,
output: File,
contentResolver: ContentResolver
): ImageExportResult {
copyFile(source, output, contentResolver)
val exifInterface = ExifInterface(output.absolutePath)
val imageRotation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)
val isRotatedLandscape = (imageRotation == ExifInterface.ORIENTATION_ROTATE_90 || imageRotation == ExifInterface.ORIENTATION_ROTATE_270)
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(output.absolutePath, options)
// Image will be rotated to orientation suggested by the exif data, because of that the width and height
// returned by the picker should be switched if the image is rotated 90 or 270 degrees.
val width: Int = if (isRotatedLandscape) options.outHeight else options.outWidth
val height: Int = if (isRotatedLandscape) options.outWidth else options.outHeight
return ImageExportResult(
width,
height,
output
)
}
}

View File

@@ -0,0 +1,8 @@
package expo.modules.imagepicker.fileprovider
import androidx.core.content.FileProvider
/**
* Dummy class for proving files for this module.
*/
class ImagePickerFileProvider : FileProvider()

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="expo_files" path="." />
<cache-path name="cached_expo_files" path="." />
</paths>

View File

@@ -0,0 +1 @@
module.exports = require('./plugin/build/withImagePicker');

View File

@@ -0,0 +1,3 @@
declare const _default: any;
export default _default;
//# sourceMappingURL=ExponentImagePicker.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExponentImagePicker.d.ts","sourceRoot":"","sources":["../src/ExponentImagePicker.ts"],"names":[],"mappings":";AACA,wBAA0D"}

View File

@@ -0,0 +1,3 @@
import { requireNativeModule } from 'expo-modules-core';
export default requireNativeModule('ExponentImagePicker');
//# sourceMappingURL=ExponentImagePicker.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExponentImagePicker.js","sourceRoot":"","sources":["../src/ExponentImagePicker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,eAAe,mBAAmB,CAAC,qBAAqB,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from 'expo-modules-core';\nexport default requireNativeModule('ExponentImagePicker');\n"]}

View File

@@ -0,0 +1,20 @@
import { PermissionResponse } from 'expo-modules-core';
import { ImagePickerResult, MediaTypeOptions } from './ImagePicker.types';
declare const _default: {
launchImageLibraryAsync({ mediaTypes, allowsMultipleSelection, base64, }: {
mediaTypes?: MediaTypeOptions | undefined;
allowsMultipleSelection?: boolean | undefined;
base64?: boolean | undefined;
}): Promise<ImagePickerResult>;
launchCameraAsync({ mediaTypes, allowsMultipleSelection, base64, }: {
mediaTypes?: MediaTypeOptions | undefined;
allowsMultipleSelection?: boolean | undefined;
base64?: boolean | undefined;
}): Promise<ImagePickerResult>;
getCameraPermissionsAsync(): Promise<PermissionResponse>;
requestCameraPermissionsAsync(): Promise<PermissionResponse>;
getMediaLibraryPermissionsAsync(_writeOnly: boolean): Promise<PermissionResponse>;
requestMediaLibraryPermissionsAsync(_writeOnly: boolean): Promise<PermissionResponse>;
};
export default _default;
//# sourceMappingURL=ExponentImagePicker.web.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ExponentImagePicker.web.d.ts","sourceRoot":"","sources":["../src/ExponentImagePicker.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAA8B,MAAM,mBAAmB,CAAC;AAEnF,OAAO,EAEL,iBAAiB,EACjB,gBAAgB,EAEjB,MAAM,qBAAqB,CAAC;;;;;;QAavB,QAAQ,iBAAiB,CAAC;;;;;QAgB1B,QAAQ,iBAAiB,CAAC;;;gDA2BoB,OAAO;oDAGH,OAAO,GAAG,QAAQ,kBAAkB,CAAC;;AAnD7F,wBAsDE"}

View File

@@ -0,0 +1,126 @@
import { PermissionStatus, Platform } from 'expo-modules-core';
import { MediaTypeOptions, } from './ImagePicker.types';
const MediaTypeInput = {
[MediaTypeOptions.All]: 'video/mp4,video/quicktime,video/x-m4v,video/*,image/*',
[MediaTypeOptions.Images]: 'image/*',
[MediaTypeOptions.Videos]: 'video/mp4,video/quicktime,video/x-m4v,video/*',
};
export default {
async launchImageLibraryAsync({ mediaTypes = MediaTypeOptions.Images, allowsMultipleSelection = false, base64 = false, }) {
// SSR guard
if (!Platform.isDOMAvailable) {
return { canceled: true, assets: null };
}
return await openFileBrowserAsync({
mediaTypes,
allowsMultipleSelection,
base64,
});
},
async launchCameraAsync({ mediaTypes = MediaTypeOptions.Images, allowsMultipleSelection = false, base64 = false, }) {
// SSR guard
if (!Platform.isDOMAvailable) {
return { canceled: true, assets: null };
}
return await openFileBrowserAsync({
mediaTypes,
allowsMultipleSelection,
capture: true,
base64,
});
},
/*
* Delegate to expo-permissions to request camera permissions
*/
async getCameraPermissionsAsync() {
return permissionGrantedResponse();
},
async requestCameraPermissionsAsync() {
return permissionGrantedResponse();
},
/*
* Camera roll permissions don't need to be requested on web, so we always
* respond with granted.
*/
async getMediaLibraryPermissionsAsync(_writeOnly) {
return permissionGrantedResponse();
},
async requestMediaLibraryPermissionsAsync(_writeOnly) {
return permissionGrantedResponse();
},
};
function permissionGrantedResponse() {
return {
status: PermissionStatus.GRANTED,
expires: 'never',
granted: true,
canAskAgain: true,
};
}
function openFileBrowserAsync({ mediaTypes, capture = false, allowsMultipleSelection = false, base64, }) {
const mediaTypeFormat = MediaTypeInput[mediaTypes];
const input = document.createElement('input');
input.style.display = 'none';
input.setAttribute('type', 'file');
input.setAttribute('accept', mediaTypeFormat);
input.setAttribute('id', String(Math.random()));
if (allowsMultipleSelection) {
input.setAttribute('multiple', 'multiple');
}
if (capture) {
input.setAttribute('capture', 'camera');
}
document.body.appendChild(input);
return new Promise((resolve) => {
input.addEventListener('change', async () => {
if (input.files) {
const files = allowsMultipleSelection ? input.files : [input.files[0]];
const assets = await Promise.all(Array.from(files).map((file) => readFile(file, { base64 })));
resolve({ canceled: false, assets });
}
else {
resolve({ canceled: true, assets: null });
}
document.body.removeChild(input);
});
const event = new MouseEvent('click');
input.dispatchEvent(event);
});
}
function readFile(targetFile, options) {
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, 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,
mimeType: targetFile.type,
fileName: targetFile.name,
// 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.base64 && { base64: uri.substr(uri.indexOf(',') + 1) }),
});
};
image.onerror = () => returnRaw();
}
else {
returnRaw();
}
};
reader.readAsDataURL(targetFile);
});
}
//# sourceMappingURL=ExponentImagePicker.web.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,100 @@
import { PermissionStatus, PermissionExpiration, PermissionHookOptions, PermissionResponse } from 'expo-modules-core';
import { CameraPermissionResponse, MediaLibraryPermissionResponse, ImagePickerResult, ImagePickerErrorResult, ImagePickerOptions } from './ImagePicker.types';
/**
* Checks user's permissions for accessing camera.
* @return A promise that fulfills with an object of type [CameraPermissionResponse](#camerapermissionresponse).
*/
export declare function getCameraPermissionsAsync(): Promise<CameraPermissionResponse>;
/**
* Checks user's permissions for accessing photos.
* @param writeOnly Whether to request write or read and write permissions. Defaults to `false`
* @return A promise that fulfills with an object of type [MediaLibraryPermissionResponse](#medialibrarypermissionresponse).
*/
export declare function getMediaLibraryPermissionsAsync(writeOnly?: boolean): Promise<MediaLibraryPermissionResponse>;
/**
* Asks the user to grant permissions for accessing camera. This does nothing on web because the
* browser camera is not used.
* @return A promise that fulfills with an object of type [CameraPermissionResponse](#camerarollpermissionresponse).
*/
export declare function requestCameraPermissionsAsync(): Promise<CameraPermissionResponse>;
/**
* Asks the user to grant permissions for accessing user's photo. This method does nothing on web.
* @param writeOnly Whether to request write or read and write permissions. Defaults to `false`
* @return A promise that fulfills with an object of type [MediaLibraryPermissionResponse](#medialibrarypermissionresponse).
*/
export declare function requestMediaLibraryPermissionsAsync(writeOnly?: boolean): Promise<MediaLibraryPermissionResponse>;
/**
* Check or request permissions to access the media library.
* This uses both `requestMediaLibraryPermissionsAsync` and `getMediaLibraryPermissionsAsync` to interact with the permissions.
*
* @example
* ```ts
* const [status, requestPermission] = ImagePicker.useMediaLibraryPermissions();
* ```
*/
export declare const useMediaLibraryPermissions: (options?: PermissionHookOptions<{
writeOnly?: boolean | undefined;
}> | undefined) => [MediaLibraryPermissionResponse | null, () => Promise<MediaLibraryPermissionResponse>, () => Promise<MediaLibraryPermissionResponse>];
/**
* Check or request permissions to access the camera.
* This uses both `requestCameraPermissionsAsync` and `getCameraPermissionsAsync` to interact with the permissions.
*
* @example
* ```ts
* const [status, requestPermission] = ImagePicker.useCameraPermissions();
* ```
*/
export declare const useCameraPermissions: (options?: PermissionHookOptions<object> | undefined) => [PermissionResponse | null, () => Promise<PermissionResponse>, () => Promise<PermissionResponse>];
/**
* Android system sometimes kills the `MainActivity` after the `ImagePicker` finishes. When this
* happens, we lost the data selected from the `ImagePicker`. However, you can retrieve the lost
* data by calling `getPendingResultAsync`. You can test this functionality by turning on
* `Don't keep activities` in the developer options.
* @return
* - **On Android:** a promise that resolves to an array of objects of exactly same type as in
* `ImagePicker.launchImageLibraryAsync` or `ImagePicker.launchCameraAsync` if the `ImagePicker`
* finished successfully. Otherwise, to the array of [`ImagePickerErrorResult`](#imagepickerimagepickererrorresult).
* - **On other platforms:** an empty array.
*/
export declare function getPendingResultAsync(): Promise<(ImagePickerResult | ImagePickerErrorResult)[]>;
/**
* Display the system UI for taking a photo with the camera. Requires `Permissions.CAMERA`.
* On Android and iOS 10 `Permissions.CAMERA_ROLL` is also required. On mobile web, this must be
* called immediately in a user interaction like a button press, otherwise the browser will block
* the request without a warning.
* > **Note:** Make sure that you handle `MainActivity` destruction on **Android**. See [ImagePicker.getPendingResultAsync](#imagepickergetpendingresultasync).
* > **Notes for Web:** The system UI can only be shown after user activation (e.g. a `Button` press).
* Therefore, calling `launchCameraAsync` in `componentDidMount`, for example, will **not** work as
* intended. The `cancelled` event will not be returned in the browser due to platform restrictions
* and inconsistencies across browsers.
* @param options An `ImagePickerOptions` object.
* @return A promise that resolves to an object with `canceled` and `assets` fields.
* When the user canceled the action the `assets` is always `null`, otherwise it's an array of
* the selected media assets which have a form of [`ImagePickerAsset`](#imagepickerasset).
*/
export declare function launchCameraAsync(options?: ImagePickerOptions): Promise<ImagePickerResult>;
/**
* Display the system UI for choosing an image or a video from the phone's library.
* Requires `Permissions.MEDIA_LIBRARY` on iOS 10 only. On mobile web, this must be called
* immediately in a user interaction like a button press, otherwise the browser will block the
* request without a warning.
*
* **Animated GIFs support:** On Android, if the selected image is an animated GIF, the result image will be an
* animated GIF too if and only if `quality` is explicitly set to `1.0` and `allowsEditing` is set to `false`.
* Otherwise compression and/or cropper will pick the first frame of the GIF and return it as the
* result (on Android the result will be a PNG). On iOS, both quality and cropping are supported.
*
* > **Notes for Web:** The system UI can only be shown after user activation (e.g. a `Button` press).
* Therefore, calling `launchImageLibraryAsync` in `componentDidMount`, for example, will **not**
* work as intended. The `cancelled` event will not be returned in the browser due to platform
* restrictions and inconsistencies across browsers.
* @param options An object extended by [`ImagePickerOptions`](#imagepickeroptions).
* @return A promise that resolves to an object with `canceled` and `assets` fields.
* When the user canceled the action the `assets` is always `null`, otherwise it's an array of
* the selected media assets which have a form of [`ImagePickerAsset`](#imagepickerasset).
*/
export declare function launchImageLibraryAsync(options?: ImagePickerOptions): Promise<ImagePickerResult>;
export * from './ImagePicker.types';
export type { PermissionExpiration, PermissionHookOptions, PermissionResponse };
export { PermissionStatus };
//# sourceMappingURL=ImagePicker.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ImagePicker.d.ts","sourceRoot":"","sources":["../src/ImagePicker.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EAInB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,wBAAwB,EACxB,8BAA8B,EAC9B,iBAAiB,EACjB,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,qBAAqB,CAAC;AAkC7B;;;GAGG;AACH,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAEnF;AAGD;;;;GAIG;AACH,wBAAsB,+BAA+B,CACnD,SAAS,GAAE,OAAe,GACzB,OAAO,CAAC,8BAA8B,CAAC,CAEzC;AAGD;;;;GAIG;AACH,wBAAsB,6BAA6B,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAEvF;AAGD;;;;GAIG;AACH,wBAAsB,mCAAmC,CACvD,SAAS,GAAE,OAAe,GACzB,OAAO,CAAC,8BAA8B,CAAC,CAGzC;AAGD;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B;;wJAOrC,CAAC;AAGH;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,4JAG/B,CAAC;AAGH;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CACpD,CAAC,iBAAiB,GAAG,sBAAsB,CAAC,EAAE,CAC/C,CAKA;AAGD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAK5B;AAGD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CAY5B;AAED,cAAc,qBAAqB,CAAC;AAEpC,YAAY,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,CAAC;AAChF,OAAO,EAAE,gBAAgB,EAAE,CAAC"}

View File

@@ -0,0 +1,158 @@
import { PermissionStatus, createPermissionHook, UnavailabilityError, CodedError, } from 'expo-modules-core';
import ExponentImagePicker from './ExponentImagePicker';
function validateOptions(options) {
const { aspect, quality, videoMaxDuration } = options;
if (aspect != null) {
const [x, y] = aspect;
if (x <= 0 || y <= 0) {
throw new CodedError('ERR_INVALID_ARGUMENT', `Invalid aspect ratio values ${x}:${y}. Provide positive numbers.`);
}
}
if (quality && (quality < 0 || quality > 1)) {
throw new CodedError('ERR_INVALID_ARGUMENT', `Invalid 'quality' value ${quality}. Provide a value between 0 and 1.`);
}
if (videoMaxDuration && videoMaxDuration < 0) {
throw new CodedError('ERR_INVALID_ARGUMENT', `Invalid 'videoMaxDuration' value ${videoMaxDuration}. Provide a non-negative number.`);
}
return options;
}
// @needsAudit
/**
* Checks user's permissions for accessing camera.
* @return A promise that fulfills with an object of type [CameraPermissionResponse](#camerapermissionresponse).
*/
export async function getCameraPermissionsAsync() {
return ExponentImagePicker.getCameraPermissionsAsync();
}
// @needsAudit
/**
* Checks user's permissions for accessing photos.
* @param writeOnly Whether to request write or read and write permissions. Defaults to `false`
* @return A promise that fulfills with an object of type [MediaLibraryPermissionResponse](#medialibrarypermissionresponse).
*/
export async function getMediaLibraryPermissionsAsync(writeOnly = false) {
return ExponentImagePicker.getMediaLibraryPermissionsAsync(writeOnly);
}
// @needsAudit
/**
* Asks the user to grant permissions for accessing camera. This does nothing on web because the
* browser camera is not used.
* @return A promise that fulfills with an object of type [CameraPermissionResponse](#camerarollpermissionresponse).
*/
export async function requestCameraPermissionsAsync() {
return ExponentImagePicker.requestCameraPermissionsAsync();
}
// @needsAudit
/**
* Asks the user to grant permissions for accessing user's photo. This method does nothing on web.
* @param writeOnly Whether to request write or read and write permissions. Defaults to `false`
* @return A promise that fulfills with an object of type [MediaLibraryPermissionResponse](#medialibrarypermissionresponse).
*/
export async function requestMediaLibraryPermissionsAsync(writeOnly = false) {
const imagePickerMethod = ExponentImagePicker.requestMediaLibraryPermissionsAsync;
return imagePickerMethod(writeOnly);
}
// @needsAudit
/**
* Check or request permissions to access the media library.
* This uses both `requestMediaLibraryPermissionsAsync` and `getMediaLibraryPermissionsAsync` to interact with the permissions.
*
* @example
* ```ts
* const [status, requestPermission] = ImagePicker.useMediaLibraryPermissions();
* ```
*/
export const useMediaLibraryPermissions = createPermissionHook({
// TODO(cedric): permission requesters should have an options param or a different requester
getMethod: (options) => getMediaLibraryPermissionsAsync(options?.writeOnly),
requestMethod: (options) => requestMediaLibraryPermissionsAsync(options?.writeOnly),
});
// @needsAudit
/**
* Check or request permissions to access the camera.
* This uses both `requestCameraPermissionsAsync` and `getCameraPermissionsAsync` to interact with the permissions.
*
* @example
* ```ts
* const [status, requestPermission] = ImagePicker.useCameraPermissions();
* ```
*/
export const useCameraPermissions = createPermissionHook({
getMethod: getCameraPermissionsAsync,
requestMethod: requestCameraPermissionsAsync,
});
// @needsAudit
/**
* Android system sometimes kills the `MainActivity` after the `ImagePicker` finishes. When this
* happens, we lost the data selected from the `ImagePicker`. However, you can retrieve the lost
* data by calling `getPendingResultAsync`. You can test this functionality by turning on
* `Don't keep activities` in the developer options.
* @return
* - **On Android:** a promise that resolves to an array of objects of exactly same type as in
* `ImagePicker.launchImageLibraryAsync` or `ImagePicker.launchCameraAsync` if the `ImagePicker`
* finished successfully. Otherwise, to the array of [`ImagePickerErrorResult`](#imagepickerimagepickererrorresult).
* - **On other platforms:** an empty array.
*/
export async function getPendingResultAsync() {
if (ExponentImagePicker.getPendingResultAsync) {
return ExponentImagePicker.getPendingResultAsync();
}
return [];
}
// @needsAudit
/**
* Display the system UI for taking a photo with the camera. Requires `Permissions.CAMERA`.
* On Android and iOS 10 `Permissions.CAMERA_ROLL` is also required. On mobile web, this must be
* called immediately in a user interaction like a button press, otherwise the browser will block
* the request without a warning.
* > **Note:** Make sure that you handle `MainActivity` destruction on **Android**. See [ImagePicker.getPendingResultAsync](#imagepickergetpendingresultasync).
* > **Notes for Web:** The system UI can only be shown after user activation (e.g. a `Button` press).
* Therefore, calling `launchCameraAsync` in `componentDidMount`, for example, will **not** work as
* intended. The `cancelled` event will not be returned in the browser due to platform restrictions
* and inconsistencies across browsers.
* @param options An `ImagePickerOptions` object.
* @return A promise that resolves to an object with `canceled` and `assets` fields.
* When the user canceled the action the `assets` is always `null`, otherwise it's an array of
* the selected media assets which have a form of [`ImagePickerAsset`](#imagepickerasset).
*/
export async function launchCameraAsync(options = {}) {
if (!ExponentImagePicker.launchCameraAsync) {
throw new UnavailabilityError('ImagePicker', 'launchCameraAsync');
}
return await ExponentImagePicker.launchCameraAsync(validateOptions(options));
}
// @needsAudit
/**
* Display the system UI for choosing an image or a video from the phone's library.
* Requires `Permissions.MEDIA_LIBRARY` on iOS 10 only. On mobile web, this must be called
* immediately in a user interaction like a button press, otherwise the browser will block the
* request without a warning.
*
* **Animated GIFs support:** On Android, if the selected image is an animated GIF, the result image will be an
* animated GIF too if and only if `quality` is explicitly set to `1.0` and `allowsEditing` is set to `false`.
* Otherwise compression and/or cropper will pick the first frame of the GIF and return it as the
* result (on Android the result will be a PNG). On iOS, both quality and cropping are supported.
*
* > **Notes for Web:** The system UI can only be shown after user activation (e.g. a `Button` press).
* Therefore, calling `launchImageLibraryAsync` in `componentDidMount`, for example, will **not**
* work as intended. The `cancelled` event will not be returned in the browser due to platform
* restrictions and inconsistencies across browsers.
* @param options An object extended by [`ImagePickerOptions`](#imagepickeroptions).
* @return A promise that resolves to an object with `canceled` and `assets` fields.
* When the user canceled the action the `assets` is always `null`, otherwise it's an array of
* the selected media assets which have a form of [`ImagePickerAsset`](#imagepickerasset).
*/
export async function launchImageLibraryAsync(options) {
if (!ExponentImagePicker.launchImageLibraryAsync) {
throw new UnavailabilityError('ImagePicker', 'launchImageLibraryAsync');
}
if (options?.allowsEditing && options.allowsMultipleSelection) {
console.warn('[expo-image-picker] `allowsEditing` is not supported when `allowsMultipleSelection` is enabled and will be ignored.' +
"Disable either 'allowsEditing' or 'allowsMultipleSelection' in 'launchImageLibraryAsync' " +
'to fix this warning.');
}
return await ExponentImagePicker.launchImageLibraryAsync(options ?? {});
}
export * from './ImagePicker.types';
export { PermissionStatus };
//# sourceMappingURL=ImagePicker.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,482 @@
import { PermissionResponse } from 'expo-modules-core';
/**
* Alias for `PermissionResponse` type exported by `expo-modules-core`.
*/
export type CameraPermissionResponse = PermissionResponse;
/**
* Extends `PermissionResponse` type exported by `expo-modules-core`, containing additional iOS-specific field.
*/
export type MediaLibraryPermissionResponse = PermissionResponse & {
/**
* Indicates if your app has access to the whole or only part of the photo library. Possible values are:
* - `'all'` if the user granted your app access to the whole photo library
* - `'limited'` if the user granted your app access only to selected photos (only available on Android API 34+ and iOS 14.0+)
* - `'none'` if user denied or hasn't yet granted the permission
*/
accessPrivileges?: 'all' | 'limited' | 'none';
};
export declare enum MediaTypeOptions {
/**
* Images and videos.
*/
All = "All",
/**
* Only videos.
*/
Videos = "Videos",
/**
* Only images.
*/
Images = "Images"
}
export declare enum VideoExportPreset {
/**
* Resolution: __Unchanged__ •
* Video compression: __None__ •
* Audio compression: __None__
*/
Passthrough = 0,
/**
* Resolution: __Depends on the device__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
LowQuality = 1,
/**
* Resolution: __Depends on the device__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
MediumQuality = 2,
/**
* Resolution: __Depends on the device__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
HighestQuality = 3,
/**
* Resolution: __640 × 480__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
H264_640x480 = 4,
/**
* Resolution: __960 × 540__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
H264_960x540 = 5,
/**
* Resolution: __1280 × 720__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
H264_1280x720 = 6,
/**
* Resolution: __1920 × 1080__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
H264_1920x1080 = 7,
/**
* Resolution: __3840 × 2160__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
H264_3840x2160 = 8,
/**
* Resolution: __1920 × 1080__ •
* Video compression: __HEVC__ •
* Audio compression: __AAC__
*/
HEVC_1920x1080 = 9,
/**
* Resolution: __3840 × 2160__ •
* Video compression: __HEVC__ •
* Audio compression: __AAC__
*/
HEVC_3840x2160 = 10
}
export declare enum UIImagePickerControllerQualityType {
/**
* Highest available resolution.
*/
High = 0,
/**
* Depends on the device.
*/
Medium = 1,
/**
* Depends on the device.
*/
Low = 2,
/**
* 640 × 480
*/
VGA640x480 = 3,
/**
* 1280 × 720
*/
IFrame1280x720 = 4,
/**
* 960 × 540
*/
IFrame960x540 = 5
}
/**
* Picker presentation style. Its values are directly mapped to the [`UIModalPresentationStyle`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621355-modalpresentationstyle).
*
* @platform ios
*/
export declare enum UIImagePickerPresentationStyle {
/**
* A presentation style in which the presented picker covers the screen.
*/
FULL_SCREEN = "fullScreen",
/**
* A presentation style that partially covers the underlying content.
*/
PAGE_SHEET = "pageSheet",
/**
* A presentation style that displays the picker centered in the screen.
*/
FORM_SHEET = "formSheet",
/**
* A presentation style where the picker is displayed over the app's content.
*/
CURRENT_CONTEXT = "currentContext",
/**
* A presentation style in which the picker view covers the screen.
*/
OVER_FULL_SCREEN = "overFullScreen",
/**
* A presentation style where the picker is displayed over the app's content.
*/
OVER_CURRENT_CONTEXT = "overCurrentContext",
/**
* A presentation style where the picker is displayed in a popover view.
*/
POPOVER = "popover",
/**
* The default presentation style chosen by the system.
* On older iOS versions, falls back to `WebBrowserPresentationStyle.FullScreen`.
*
* @platform ios
*/
AUTOMATIC = "automatic"
}
/**
* Picker preferred asset representation mode. Its values are directly mapped to the [`PHPickerConfigurationAssetRepresentationMode`](https://developer.apple.com/documentation/photokit/phpickerconfigurationassetrepresentationmode).
*
* @platform ios
*/
export declare enum UIImagePickerPreferredAssetRepresentationMode {
/**
* A mode that indicates that the system chooses the appropriate asset representation.
*/
Automatic = "automatic",
/**
* A mode that uses the most compatible asset representation.
*/
Compatible = "compatible",
/**
* A mode that uses the current representation to avoid transcoding, if possible.
*/
Current = "current"
}
export declare enum CameraType {
/**
* Back/rear camera.
*/
back = "back",
/**
* Front camera
*/
front = "front"
}
/**
* @hidden
* @deprecated Use `ImagePickerAsset` instead
*/
export type ImageInfo = ImagePickerAsset;
/**
* Represents an asset (image or video) returned by the image picker or camera.
*/
export type ImagePickerAsset = {
/**
* URI to the local image or video file (usable as the source of an `Image` element, in the case of
* an image) and `width` and `height` specify the dimensions of the media.
*/
uri: string;
/**
* The unique ID that represents the picked image or video, if picked from the library. It can be used
* by [expo-media-library](./media-library) to manage the picked asset.
*
* > This might be `null` when the ID is unavailable or the user gave limited permission to access the media library.
* > On Android, the ID is unavailable when the user selects a photo by directly browsing file system.
*
* @platform ios
* @platform android
*/
assetId?: string | null;
/**
* Width of the image or video.
*/
width: number;
/**
* Height of the image or video.
*/
height: number;
/**
* The type of the asset.
*/
type?: 'image' | 'video';
/**
* Preferred filename to use when saving this item. This might be `null` when the name is unavailable
* or user gave limited permission to access the media library.
*
*/
fileName?: string | null;
/**
* File size of the picked image or video, in bytes.
*
*/
fileSize?: number;
/**
* The `exif` field is included if the `exif` option is truthy, and is an object containing the
* image's EXIF data. The names of this object's properties are EXIF tags and the values are the
* respective EXIF values for those tags.
*/
exif?: Record<string, any> | null;
/**
* When the `base64` option is truthy, it is a Base64-encoded string of the selected image's JPEG data, otherwise `null`.
* If you prepend this with `'data:image/jpeg;base64,'` to create a data URI,
* you can use it as the source of an `Image` element; for example:
* ```ts
* <Image
* source={{ uri: 'data:image/jpeg;base64,' + asset.base64 }}
* style={{ width: 200, height: 200 }}
* />
* ```
*/
base64?: string | null;
/**
* Length of the video in milliseconds or `null` if the asset is not a video.
*/
duration?: number | null;
/**
* The MIME type of the selected asset or `null` if could not be determined.
*/
mimeType?: string;
};
export type ImagePickerErrorResult = {
/**
* The error code.
*/
code: string;
/**
* The error message.
*/
message: string;
/**
* The exception which caused the error.
*/
exception?: string;
};
/**
* Type representing successful and canceled pick result.
*/
export type ImagePickerResult = ImagePickerSuccessResult | ImagePickerCanceledResult;
/**
* Type representing successful pick result.
*/
export type ImagePickerSuccessResult = {
/**
* Boolean flag set to `false` showing that the request was successful.
*/
canceled: false;
/**
* An array of picked assets.
*/
assets: ImagePickerAsset[];
};
/**
* Type representing canceled pick result.
*/
export type ImagePickerCanceledResult = {
/**
* Boolean flag set to `true` showing that the request was canceled.
*/
canceled: true;
/**
* `null` signifying that the request was canceled.
*/
assets: null;
};
/**
* @hidden
* @deprecated Use `ImagePickerResult` instead.
*/
export type ImagePickerCancelledResult = ImagePickerCanceledResult;
/**
* @hidden
* @deprecated `ImagePickerMultipleResult` has been deprecated in favor of `ImagePickerResult`.
*/
export type ImagePickerMultipleResult = ImagePickerResult;
export type ImagePickerOptions = {
/**
* Whether to show a UI to edit the image after it is picked. On Android the user can crop and
* rotate the image and on iOS simply crop it.
*
* > - Cropping multiple images is not supported - this option is mutually exclusive with `allowsMultipleSelection`.
* > - On iOS, this option is ignored if `allowsMultipleSelection` is enabled.
* > - On iOS cropping a `.bmp` image will convert it to `.png`.
*
* @default false
* @platform ios
* @platform android
*/
allowsEditing?: boolean;
/**
* An array with two entries `[x, y]` specifying the aspect ratio to maintain if the user is
* allowed to edit the image (by passing `allowsEditing: true`). This is only applicable on
* Android, since on iOS the crop rectangle is always a square.
*/
aspect?: [number, number];
/**
* Specify the quality of compression, from `0` to `1`. `0` means compress for small size,
* `1` means compress for maximum quality.
* > Note: If the selected image has been compressed before, the size of the output file may be
* > bigger than the size of the original image.
*
* > Note: On iOS, if a `.bmp` or `.png` image is selected from the library, this option is ignored.
*
* @default 0.2
* @platform ios
* @platform android
*/
quality?: number;
/**
* Choose what type of media to pick.
* @default ImagePicker.MediaTypeOptions.Images
*/
mediaTypes?: MediaTypeOptions;
/**
* Whether to also include the EXIF data for the image. On iOS the EXIF data does not include GPS
* tags in the camera case.
*/
exif?: boolean;
/**
* Whether to also include the image data in Base64 format.
*/
base64?: boolean;
/**
* Specify preset which will be used to compress selected video.
* @default ImagePicker.VideoExportPreset.Passthrough
* @platform ios 11+
* @deprecated See [`videoExportPreset`](https://developer.apple.com/documentation/uikit/uiimagepickercontroller/2890964-videoexportpreset?language=objc)
* in Apple documentation.
*/
videoExportPreset?: VideoExportPreset;
/**
* Specify the quality of recorded videos. Defaults to the highest quality available for the device.
* @default ImagePicker.UIImagePickerControllerQualityType.High
* @platform ios
*/
videoQuality?: UIImagePickerControllerQualityType;
/**
* Whether or not to allow selecting multiple media files at once.
*
* > Cropping multiple images is not supported - this option is mutually exclusive with `allowsEditing`.
* > If this option is enabled, then `allowsEditing` is ignored.
*
* @default false
* @platform ios 14+
* @platform android
* @platform web
*/
allowsMultipleSelection?: boolean;
/**
* The maximum number of items that user can select. Applicable when `allowsMultipleSelection` is enabled.
* Setting the value to `0` sets the selection limit to the maximum that the system supports.
*
* @platform ios 14+
* @platform android
* @default 0
*/
selectionLimit?: number;
/**
* Whether to display number badges when assets are selected. The badges are numbered
* in selection order. Assets are then returned in the exact same order they were selected.
*
* > Assets should be returned in the selection order regardless of this option,
* > but there is no guarantee that it is always true when this option is disabled.
*
* @platform ios 15+
* @default false
*/
orderedSelection?: boolean;
/**
* Maximum duration, in seconds, for video recording. Setting this to `0` disables the limit.
* Defaults to `0` (no limit).
* - **On iOS**, when `allowsEditing` is set to `true`, maximum duration is limited to 10 minutes.
* This limit is applied automatically, if `0` or no value is specified.
* - **On Android**, effect of this option depends on support of installed camera app.
* - **On Web** this option has no effect - the limit is browser-dependant.
*/
videoMaxDuration?: number;
/**
* Choose [presentation style](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621355-modalpresentationstyle?language=objc)
* to customize view during taking photo/video.
* @default ImagePicker.UIImagePickerPresentationStyle.Automatic
* @platform ios
*/
presentationStyle?: UIImagePickerPresentationStyle;
/**
* Selects the camera-facing type. The `CameraType` enum provides two options:
* `front` for the front-facing camera and `back` for the back-facing camera.
* - **On Android**, the behavior of this option may vary based on the camera app installed on the device.
* @default CameraType.back
* @platform ios
* @platform android
*/
cameraType?: CameraType;
/**
* Choose [preferred asset representation mode](https://developer.apple.com/documentation/photokit/phpickerconfigurationassetrepresentationmode)
* to use when loading assets.
* @default ImagePicker.UIImagePickerPreferredAssetRepresentationMode.Automatic
* @platform ios 14+
*/
preferredAssetRepresentationMode?: UIImagePickerPreferredAssetRepresentationMode;
/**
* Uses the legacy image picker on Android. This will allow media to be selected from outside the users photo library.
* @platform android
* @default false
*/
legacy?: boolean;
};
export type OpenFileBrowserOptions = {
/**
* Choose what type of media to pick.
* @default ImagePicker.MediaTypeOptions.Images
*/
mediaTypes: MediaTypeOptions;
capture?: boolean;
/**
* Whether or not to allow selecting multiple media files at once.
* @platform web
*/
allowsMultipleSelection: boolean;
/**
* Whether to also include the image data in Base64 format.
*/
base64: boolean;
};
/**
* @hidden
* @deprecated Use `ImagePickerResult` or `OpenFileBrowserOptions` instead.
*/
export type ExpandImagePickerResult<T extends ImagePickerOptions | OpenFileBrowserOptions> = T extends {
allowsMultipleSelection: true;
} ? ImagePickerResult : ImagePickerResult;
//# sourceMappingURL=ImagePicker.types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ImagePicker.types.d.ts","sourceRoot":"","sources":["../src/ImagePicker.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGvD;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,kBAAkB,CAAC;AAG1D;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG,kBAAkB,GAAG;IAChE;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;CAC/C,CAAC;AAGF,oBAAY,gBAAgB;IAC1B;;OAEG;IACH,GAAG,QAAQ;IACX;;OAEG;IACH,MAAM,WAAW;IACjB;;OAEG;IACH,MAAM,WAAW;CAClB;AAGD,oBAAY,iBAAiB;IAC3B;;;;OAIG;IACH,WAAW,IAAI;IACf;;;;OAIG;IACH,UAAU,IAAI;IACd;;;;OAIG;IACH,aAAa,IAAI;IACjB;;;;OAIG;IACH,cAAc,IAAI;IAClB;;;;OAIG;IACH,YAAY,IAAI;IAChB;;;;OAIG;IACH,YAAY,IAAI;IAChB;;;;OAIG;IACH,aAAa,IAAI;IACjB;;;;OAIG;IACH,cAAc,IAAI;IAClB;;;;OAIG;IACH,cAAc,IAAI;IAClB;;;;OAIG;IACH,cAAc,IAAI;IAClB;;;;OAIG;IACH,cAAc,KAAK;CACpB;AAGD,oBAAY,kCAAkC;IAC5C;;OAEG;IACH,IAAI,IAAI;IACR;;OAEG;IACH,MAAM,IAAI;IACV;;OAEG;IACH,GAAG,IAAI;IACP;;OAEG;IACH,UAAU,IAAI;IACd;;OAEG;IACH,cAAc,IAAI;IAClB;;OAEG;IACH,aAAa,IAAI;CAClB;AAED;;;;GAIG;AACH,oBAAY,8BAA8B;IACxC;;OAEG;IACH,WAAW,eAAe;IAC1B;;OAEG;IACH,UAAU,cAAc;IACxB;;OAEG;IACH,UAAU,cAAc;IACxB;;OAEG;IACH,eAAe,mBAAmB;IAClC;;OAEG;IACH,gBAAgB,mBAAmB;IACnC;;OAEG;IACH,oBAAoB,uBAAuB;IAC3C;;OAEG;IACH,OAAO,YAAY;IACnB;;;;;OAKG;IACH,SAAS,cAAc;CACxB;AAED;;;;GAIG;AACH,oBAAY,6CAA6C;IACvD;;OAEG;IACH,SAAS,cAAc;IACvB;;OAEG;IACH,UAAU,eAAe;IACzB;;OAEG;IACH,OAAO,YAAY;CACpB;AAED,oBAAY,UAAU;IACpB;;OAEG;IACH,IAAI,SAAS;IACb;;OAEG;IACH,KAAK,UAAU;CAChB;AAED;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,gBAAgB,CAAC;AAEzC;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IAClC;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAGF,MAAM,MAAM,sBAAsB,GAAG;IACnC;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAGF;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,wBAAwB,GAAG,yBAAyB,CAAC;AAErF;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC;;OAEG;IACH,QAAQ,EAAE,KAAK,CAAC;IAChB;;OAEG;IACH,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC5B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC;;OAEG;IACH,QAAQ,EAAE,IAAI,CAAC;IACf;;OAEG;IACH,MAAM,EAAE,IAAI,CAAC;CACd,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,0BAA0B,GAAG,yBAAyB,CAAC;AAEnE;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,iBAAiB,CAAC;AAG1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC;;;;OAIG;IACH,YAAY,CAAC,EAAE,kCAAkC,CAAC;IAClD;;;;;;;;;;OAUG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,8BAA8B,CAAC;IACnD;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;;;;;OAKG;IACH,gCAAgC,CAAC,EAAE,6CAA6C,CAAC;IACjF;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAGF,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;OAGG;IACH,UAAU,EAAE,gBAAgB,CAAC;IAE7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,uBAAuB,EAAE,OAAO,CAAC;IACjC;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,uBAAuB,CAAC,CAAC,SAAS,kBAAkB,GAAG,sBAAsB,IACvF,CAAC,SAAS;IACR,uBAAuB,EAAE,IAAI,CAAC;CAC/B,GACG,iBAAiB,GACjB,iBAAiB,CAAC"}

View File

@@ -0,0 +1,189 @@
// @needsAudit
export var MediaTypeOptions;
(function (MediaTypeOptions) {
/**
* Images and videos.
*/
MediaTypeOptions["All"] = "All";
/**
* Only videos.
*/
MediaTypeOptions["Videos"] = "Videos";
/**
* Only images.
*/
MediaTypeOptions["Images"] = "Images";
})(MediaTypeOptions || (MediaTypeOptions = {}));
// @needsAudit
export var VideoExportPreset;
(function (VideoExportPreset) {
/**
* Resolution: __Unchanged__ •
* Video compression: __None__ •
* Audio compression: __None__
*/
VideoExportPreset[VideoExportPreset["Passthrough"] = 0] = "Passthrough";
/**
* Resolution: __Depends on the device__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
VideoExportPreset[VideoExportPreset["LowQuality"] = 1] = "LowQuality";
/**
* Resolution: __Depends on the device__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
VideoExportPreset[VideoExportPreset["MediumQuality"] = 2] = "MediumQuality";
/**
* Resolution: __Depends on the device__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
VideoExportPreset[VideoExportPreset["HighestQuality"] = 3] = "HighestQuality";
/**
* Resolution: __640 × 480__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
VideoExportPreset[VideoExportPreset["H264_640x480"] = 4] = "H264_640x480";
/**
* Resolution: __960 × 540__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
VideoExportPreset[VideoExportPreset["H264_960x540"] = 5] = "H264_960x540";
/**
* Resolution: __1280 × 720__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
VideoExportPreset[VideoExportPreset["H264_1280x720"] = 6] = "H264_1280x720";
/**
* Resolution: __1920 × 1080__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
VideoExportPreset[VideoExportPreset["H264_1920x1080"] = 7] = "H264_1920x1080";
/**
* Resolution: __3840 × 2160__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
VideoExportPreset[VideoExportPreset["H264_3840x2160"] = 8] = "H264_3840x2160";
/**
* Resolution: __1920 × 1080__ •
* Video compression: __HEVC__ •
* Audio compression: __AAC__
*/
VideoExportPreset[VideoExportPreset["HEVC_1920x1080"] = 9] = "HEVC_1920x1080";
/**
* Resolution: __3840 × 2160__ •
* Video compression: __HEVC__ •
* Audio compression: __AAC__
*/
VideoExportPreset[VideoExportPreset["HEVC_3840x2160"] = 10] = "HEVC_3840x2160";
})(VideoExportPreset || (VideoExportPreset = {}));
// @needsAudit
export var UIImagePickerControllerQualityType;
(function (UIImagePickerControllerQualityType) {
/**
* Highest available resolution.
*/
UIImagePickerControllerQualityType[UIImagePickerControllerQualityType["High"] = 0] = "High";
/**
* Depends on the device.
*/
UIImagePickerControllerQualityType[UIImagePickerControllerQualityType["Medium"] = 1] = "Medium";
/**
* Depends on the device.
*/
UIImagePickerControllerQualityType[UIImagePickerControllerQualityType["Low"] = 2] = "Low";
/**
* 640 × 480
*/
UIImagePickerControllerQualityType[UIImagePickerControllerQualityType["VGA640x480"] = 3] = "VGA640x480";
/**
* 1280 × 720
*/
UIImagePickerControllerQualityType[UIImagePickerControllerQualityType["IFrame1280x720"] = 4] = "IFrame1280x720";
/**
* 960 × 540
*/
UIImagePickerControllerQualityType[UIImagePickerControllerQualityType["IFrame960x540"] = 5] = "IFrame960x540";
})(UIImagePickerControllerQualityType || (UIImagePickerControllerQualityType = {}));
/**
* Picker presentation style. Its values are directly mapped to the [`UIModalPresentationStyle`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621355-modalpresentationstyle).
*
* @platform ios
*/
export var UIImagePickerPresentationStyle;
(function (UIImagePickerPresentationStyle) {
/**
* A presentation style in which the presented picker covers the screen.
*/
UIImagePickerPresentationStyle["FULL_SCREEN"] = "fullScreen";
/**
* A presentation style that partially covers the underlying content.
*/
UIImagePickerPresentationStyle["PAGE_SHEET"] = "pageSheet";
/**
* A presentation style that displays the picker centered in the screen.
*/
UIImagePickerPresentationStyle["FORM_SHEET"] = "formSheet";
/**
* A presentation style where the picker is displayed over the app's content.
*/
UIImagePickerPresentationStyle["CURRENT_CONTEXT"] = "currentContext";
/**
* A presentation style in which the picker view covers the screen.
*/
UIImagePickerPresentationStyle["OVER_FULL_SCREEN"] = "overFullScreen";
/**
* A presentation style where the picker is displayed over the app's content.
*/
UIImagePickerPresentationStyle["OVER_CURRENT_CONTEXT"] = "overCurrentContext";
/**
* A presentation style where the picker is displayed in a popover view.
*/
UIImagePickerPresentationStyle["POPOVER"] = "popover";
/**
* The default presentation style chosen by the system.
* On older iOS versions, falls back to `WebBrowserPresentationStyle.FullScreen`.
*
* @platform ios
*/
UIImagePickerPresentationStyle["AUTOMATIC"] = "automatic";
})(UIImagePickerPresentationStyle || (UIImagePickerPresentationStyle = {}));
/**
* Picker preferred asset representation mode. Its values are directly mapped to the [`PHPickerConfigurationAssetRepresentationMode`](https://developer.apple.com/documentation/photokit/phpickerconfigurationassetrepresentationmode).
*
* @platform ios
*/
export var UIImagePickerPreferredAssetRepresentationMode;
(function (UIImagePickerPreferredAssetRepresentationMode) {
/**
* A mode that indicates that the system chooses the appropriate asset representation.
*/
UIImagePickerPreferredAssetRepresentationMode["Automatic"] = "automatic";
/**
* A mode that uses the most compatible asset representation.
*/
UIImagePickerPreferredAssetRepresentationMode["Compatible"] = "compatible";
/**
* A mode that uses the current representation to avoid transcoding, if possible.
*/
UIImagePickerPreferredAssetRepresentationMode["Current"] = "current";
})(UIImagePickerPreferredAssetRepresentationMode || (UIImagePickerPreferredAssetRepresentationMode = {}));
export var CameraType;
(function (CameraType) {
/**
* Back/rear camera.
*/
CameraType["back"] = "back";
/**
* Front camera
*/
CameraType["front"] = "front";
})(CameraType || (CameraType = {}));
//# sourceMappingURL=ImagePicker.types.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
{
"platforms": ["android", "ios"],
"ios": {
"modulesClassNames": ["ImagePickerModule"]
},
"android": {
"modulesClassNames": ["expo.modules.imagepicker.ImagePickerModule"]
}
}

View File

@@ -0,0 +1,32 @@
require 'json'
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
Pod::Spec.new do |s|
s.name = 'ExpoImagePicker'
s.version = package['version']
s.summary = package['description']
s.description = package['description']
s.license = package['license']
s.author = package['author']
s.homepage = package['homepage']
s.platform = :ios, '13.4'
s.swift_version = '5.4'
s.source = { git: 'https://github.com/expo/expo.git' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0')
s.source_files = "**/*.h"
s.vendored_frameworks = "#{s.name}.xcframework"
else
s.source_files = "**/*.{h,m,swift}"
end
end

View File

@@ -0,0 +1,135 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import ExpoModulesCore
internal class PermissionsModuleNotFoundException: Exception {
override var reason: String {
"Permissions module not found. Are you sure that Expo modules are properly linked?"
}
}
internal class FileSystemModuleNotFoundException: Exception {
override var reason: String {
"FileSystem module not found. Are you sure that Expo modules are properly linked?"
}
}
internal class LoggerModuleNotFoundException: Exception {
override var reason: String {
"Logger module not found. Are you sure that Expo modules are properly linked?"
}
}
internal class MissingCameraPermissionException: Exception {
override var reason: String {
"Missing camera or camera roll permission"
}
}
internal class MissingMicrophonePermissionException: Exception {
override var reason: String {
"Missing microphone permission. Please enable it with the `expo-image-picker` config plugin"
}
}
internal class MissingPhotoLibraryPermissionException: Exception {
override var reason: String {
"Missing photo library permission"
}
}
internal class CameraUnavailableOnSimulatorException: Exception {
override var reason: String {
"Camera not available on simulator"
}
}
internal class MultiselectUnavailableException: Exception {
override var reason: String {
"Multiple selection is only available on iOS 14+"
}
}
internal class MissingCurrentViewControllerException: Exception {
override var reason: String {
"Cannot determine currently presented view controller"
}
}
internal class MaxDurationWhileEditingExceededException: Exception {
override var reason: String {
"'videoMaxDuration' limits to 600 when 'allowsEditing=true'"
}
}
internal class InvalidMediaTypeException: GenericException<String?> {
override var reason: String {
"Cannot handle '\(param ?? "nil")' media type"
}
}
internal class FailedToCreateGifException: Exception {
override var reason: String {
"Failed to create image destination for GIF export"
}
}
internal class FailedToExportGifException: Exception {
override var reason: String {
"Failed to export requested GIF"
}
}
internal class FailedToWriteImageException: Exception {
override var reason: String {
"Failed to write data to a file"
}
}
internal class FailedToReadImageException: Exception {
override var reason: String {
"Failed to read picked image"
}
}
internal class FailedToReadImageDataException: Exception {
override var reason: String {
"Failed to read data from a file"
}
}
internal class FailedToReadVideoSizeException: Exception {
override var reason: String {
"Failed to read the video size"
}
}
internal class FailedToReadVideoException: Exception {
override var reason: String {
"Failed to read picked video"
}
}
internal class FailedToTranscodeVideoException: Exception {
override var reason: String {
"Failed to transcode picked video"
}
}
internal class UnsupportedVideoExportPresetException: GenericException<String> {
override var reason: String {
"Video cannot be transcoded with export preset: \(param)"
}
}
internal class FailedToPickVideoException: Exception {
override var reason: String {
"Video could not be picked"
}
}
internal class FailedToReadImageDataForBase64Exception: Exception {
override var reason: String {
"Failed to read image data to perform base64 encoding"
}
}

View File

@@ -0,0 +1,121 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import PhotosUI
/**
Protocol that describes scenarios we care about while the user is picking media.
*/
protocol OnMediaPickingResultHandler {
@available(iOS 14, *)
func didPickMultipleMedia(selection: [PHPickerResult])
func didPickMedia(mediaInfo: MediaInfo)
func didCancelPicking()
}
/**
This class is responsible for responding to any events that are happening in `UIImagePickerController`.
It then forwards them back in unified way via `OnMediaPickingResultHandler`.
The functionality of this delegate is separated from the main module class for two reasons:
1) main module cannot inherit from `NSObject` (and that's required by three protocols we must conform to),
because it already inherits from `Module` class and Swift language does not allow multiple inheritance,
2) it separates some logic from the main module class and hopefully makes it cleaner.
*/
internal class ImagePickerHandler: NSObject,
PHPickerViewControllerDelegate,
UINavigationControllerDelegate,
UIImagePickerControllerDelegate,
UIAdaptivePresentationControllerDelegate {
private let onMediaPickingResultHandler: OnMediaPickingResultHandler
private let hideStatusBarWhenPresented: Bool
private var statusBarVisibilityController = StatusBarVisibilityController()
init(onMediaPickingResultHandler: OnMediaPickingResultHandler, hideStatusBarWhenPresented: Bool) {
self.onMediaPickingResultHandler = onMediaPickingResultHandler
self.hideStatusBarWhenPresented = hideStatusBarWhenPresented
}
private func handlePickedMedia(mediaInfo: MediaInfo) {
statusBarVisibilityController.maybeRestoreStatusBarVisibility()
onMediaPickingResultHandler.didPickMedia(mediaInfo: mediaInfo)
}
@available(iOS 14, *)
private func handlePickedMedia(selection: [PHPickerResult]) {
statusBarVisibilityController.maybeRestoreStatusBarVisibility()
onMediaPickingResultHandler.didPickMultipleMedia(selection: selection)
}
private func handlePickingCancellation() {
statusBarVisibilityController.maybeRestoreStatusBarVisibility()
onMediaPickingResultHandler.didCancelPicking()
}
// MARK: - UIImagePickerControllerDelegate
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: MediaInfo) {
DispatchQueue.main.async {
picker.dismiss(animated: true) { [weak self] in
self?.handlePickedMedia(mediaInfo: info)
}
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
DispatchQueue.main.async {
picker.dismiss(animated: true) { [weak self] in
self?.handlePickingCancellation()
}
}
}
// MARK: - PHPickerViewControllerDelegate
@available(iOS 14, *)
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
DispatchQueue.main.async {
picker.dismiss(animated: true) { [weak self] in
// The PHPickerViewController returns empty collection when canceled
if results.isEmpty {
self?.handlePickingCancellation()
} else {
self?.handlePickedMedia(selection: results)
}
}
}
}
// MARK: - UIAdaptivePresentationControllerDelegate
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
handlePickingCancellation()
}
// MARK: - UINavigationControllerDelegate
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
statusBarVisibilityController.maybePreserveVisibilityAndHideStatusBar(hideStatusBarWhenPresented)
}
}
/**
Protocol that is a common type for supported picker controllers.
*/
internal protocol PickerUIController: UIViewController {
func setResultHandler(_ handler: ImagePickerHandler)
}
extension UIImagePickerController: PickerUIController {
func setResultHandler(_ handler: ImagePickerHandler) {
self.delegate = handler
self.presentationController?.delegate = handler
}
}
@available(iOS 14, *)
extension PHPickerViewController: PickerUIController {
func setResultHandler(_ handler: ImagePickerHandler) {
self.delegate = handler
self.presentationController?.delegate = handler
}
}

View File

@@ -0,0 +1,253 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import UIKit
import PhotosUI
import ExpoModulesCore
typealias MediaInfo = [UIImagePickerController.InfoKey: Any]
/**
Helper struct storing single picking operation context variables that have their own non-sharable state.
*/
struct PickingContext {
let promise: Promise
let options: ImagePickerOptions
let imagePickerHandler: ImagePickerHandler
}
enum OperationType {
case ask
case get
}
public class ImagePickerModule: Module, OnMediaPickingResultHandler {
public func definition() -> ModuleDefinition {
// TODO: (@bbarthec) change to "ExpoImagePicker" and propagate to other platforms
Name("ExponentImagePicker")
OnCreate {
self.appContext?.permissions?.register([
CameraPermissionRequester(),
MediaLibraryPermissionRequester(),
MediaLibraryWriteOnlyPermissionRequester()
])
}
AsyncFunction("getCameraPermissionsAsync", { (promise: Promise) in
self.handlePermissionRequest(requesterClass: CameraPermissionRequester.self, operationType: .get, promise: promise)
})
AsyncFunction("getMediaLibraryPermissionsAsync", { (writeOnly: Bool, promise: Promise) in
self.handlePermissionRequest(requesterClass: self.getMediaLibraryPermissionRequester(writeOnly), operationType: .get, promise: promise)
})
AsyncFunction("requestCameraPermissionsAsync", { (promise: Promise) in
self.handlePermissionRequest(requesterClass: CameraPermissionRequester.self, operationType: .ask, promise: promise)
})
AsyncFunction("requestMediaLibraryPermissionsAsync", { (writeOnly: Bool, promise: Promise) in
self.handlePermissionRequest(requesterClass: self.getMediaLibraryPermissionRequester(writeOnly), operationType: .ask, promise: promise)
})
AsyncFunction("launchCameraAsync", { (options: ImagePickerOptions, promise: Promise) -> Void in
guard let permissions = self.appContext?.permissions else {
return promise.reject(PermissionsModuleNotFoundException())
}
guard permissions.hasGrantedPermission(usingRequesterClass: CameraPermissionRequester.self) else {
return promise.reject(MissingCameraPermissionException())
}
self.launchImagePicker(sourceType: .camera, options: options, promise: promise)
})
.runOnQueue(DispatchQueue.main)
AsyncFunction("launchImageLibraryAsync", { (options: ImagePickerOptions, promise: Promise) in
self.launchImagePicker(sourceType: .photoLibrary, options: options, promise: promise)
})
.runOnQueue(DispatchQueue.main)
}
private var currentPickingContext: PickingContext?
private func handlePermissionRequest(requesterClass: AnyClass, operationType: OperationType, promise: Promise) {
guard let permissions = self.appContext?.permissions else {
return promise.reject(PermissionsModuleNotFoundException())
}
switch operationType {
case .get: permissions.getPermissionUsingRequesterClass(requesterClass, resolve: promise.resolver, reject: promise.legacyRejecter)
case .ask: permissions.askForPermission(usingRequesterClass: requesterClass, resolve: promise.resolver, reject: promise.legacyRejecter)
}
}
private func getMediaLibraryPermissionRequester(_ writeOnly: Bool) -> AnyClass {
return writeOnly ? MediaLibraryWriteOnlyPermissionRequester.self : MediaLibraryPermissionRequester.self
}
private func launchImagePicker(sourceType: UIImagePickerController.SourceType, options: ImagePickerOptions, promise: Promise) {
let imagePickerDelegate = ImagePickerHandler(onMediaPickingResultHandler: self, hideStatusBarWhenPresented: options.allowsEditing && !options.allowsMultipleSelection)
let pickingContext = PickingContext(promise: promise,
options: options,
imagePickerHandler: imagePickerDelegate)
if #available(iOS 14, *), !options.allowsEditing && sourceType != .camera {
self.launchMultiSelectPicker(pickingContext: pickingContext)
} else {
self.launchLegacyImagePicker(sourceType: sourceType, pickingContext: pickingContext)
}
}
private func launchLegacyImagePicker(sourceType: UIImagePickerController.SourceType, pickingContext: PickingContext) {
let options = pickingContext.options
let picker = UIImagePickerController()
picker.fixCannotMoveEditingBox()
if sourceType == .camera {
#if targetEnvironment(simulator)
return pickingContext.promise.reject(CameraUnavailableOnSimulatorException())
#else
picker.sourceType = .camera
picker.cameraDevice = options.cameraType == .front ? .front : .rear
#endif
}
if sourceType == .photoLibrary {
picker.sourceType = .photoLibrary
}
picker.mediaTypes = options.mediaTypes.toArray()
if options.mediaTypes.requiresMicrophonePermission() && sourceType == .camera {
do {
try checkMicrophonePermissions()
} catch {
pickingContext.promise.reject(error)
return
}
}
picker.videoExportPreset = options.videoExportPreset.toAVAssetExportPreset()
picker.videoQuality = options.videoQuality.toQualityType()
picker.videoMaximumDuration = options.videoMaxDuration
if options.allowsEditing {
picker.allowsEditing = options.allowsEditing
if options.videoMaxDuration > 600 {
return pickingContext.promise.reject(MaxDurationWhileEditingExceededException())
}
if options.videoMaxDuration == 0 {
picker.videoMaximumDuration = 600.0
}
}
presentPickerUI(picker, pickingContext: pickingContext)
}
private func checkMicrophonePermissions() throws {
guard Bundle.main.object(forInfoDictionaryKey: "NSMicrophoneUsageDescription") != nil else {
throw MissingMicrophonePermissionException()
}
}
@available(iOS 14, *)
private func launchMultiSelectPicker(pickingContext: PickingContext) {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
let options = pickingContext.options
// selection limit = 1 --> single selection, reflects the old picker behavior
configuration.selectionLimit = options.allowsMultipleSelection ? options.selectionLimit : SINGLE_SELECTION
configuration.filter = options.mediaTypes.toPickerFilter()
if #available(iOS 14, *) {
configuration.preferredAssetRepresentationMode = options.preferredAssetRepresentationMode.toAssetRepresentationMode()
}
if #available(iOS 15, *) {
configuration.selection = options.orderedSelection ? .ordered : .default
}
let picker = PHPickerViewController(configuration: configuration)
presentPickerUI(picker, pickingContext: pickingContext)
}
private func presentPickerUI(_ picker: PickerUIController, pickingContext context: PickingContext) {
guard let currentViewController = self.appContext?.utilities?.currentViewController() else {
return context.promise.reject(MissingCurrentViewControllerException())
}
picker.modalPresentationStyle = context.options.presentationStyle.toPresentationStyle()
if UIDevice.current.userInterfaceIdiom == .pad {
let viewFrame = currentViewController.view.frame
picker.popoverPresentationController?.sourceRect = CGRect(
x: viewFrame.midX,
y: viewFrame.maxY,
width: 0,
height: 0
)
picker.popoverPresentationController?.sourceView = currentViewController.view
}
picker.setResultHandler(context.imagePickerHandler)
// Store picking context as we're navigating to the different view controller (starting asynchronous flow)
self.currentPickingContext = context
currentViewController.present(picker, animated: true, completion: nil)
}
// MARK: - OnMediaPickingResultHandler
func didCancelPicking() {
self.currentPickingContext?.promise.resolve(ImagePickerResponse(assets: nil, canceled: true))
self.currentPickingContext = nil
}
@available(iOS 14, *)
func didPickMultipleMedia(selection: [PHPickerResult]) {
guard let options = self.currentPickingContext?.options,
let promise = self.currentPickingContext?.promise else {
log.error("Picking operation context has been lost.")
return
}
guard let fileSystem = self.appContext?.fileSystem else {
return promise.reject(FileSystemModuleNotFoundException())
}
let mediaHandler = MediaHandler(fileSystem: fileSystem,
options: options)
// Clean up the currently stored picking context
self.currentPickingContext = nil
mediaHandler.handleMultipleMedia(selection) { result -> Void in
switch result {
case .failure(let error): return promise.reject(error)
case .success(let response): return promise.resolve(response)
}
}
}
func didPickMedia(mediaInfo: MediaInfo) {
guard let options = self.currentPickingContext?.options,
let promise = self.currentPickingContext?.promise else {
log.error("Picking operation context has been lost.")
return
}
guard let fileSystem = self.appContext?.fileSystem else {
return promise.reject(FileSystemModuleNotFoundException())
}
// Clean up the currently stored picking context
self.currentPickingContext = nil
let mediaHandler = MediaHandler(fileSystem: fileSystem,
options: options)
mediaHandler.handleMedia(mediaInfo) { result -> Void in
switch result {
case .failure(let error): return promise.reject(error)
case .success(let response): return promise.resolve(response)
}
}
}
}

View File

@@ -0,0 +1,228 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import ExpoModulesCore
import MobileCoreServices
import PhotosUI
internal let DEFAULT_QUALITY = 0.2
internal let MAXIMUM_QUALITY = 1.0
internal let UNLIMITED_SELECTION = 0
internal let SINGLE_SELECTION = 1
internal struct ImagePickerOptions: Record {
@Field
var allowsEditing: Bool = false
@Field
var aspect: [Double]
@Field
var quality: Double?
@Field
var mediaTypes: MediaType = .images
@Field
var exif: Bool
@Field
var base64: Bool = false
@Field
var videoExportPreset: VideoExportPreset = .passthrough
@Field
var videoQuality: VideoQuality = .typeHigh
@Field
var videoMaxDuration: Double = 0
@Field
var presentationStyle: PresentationStyle = .automatic
@Field
var preferredAssetRepresentationMode: PreferredAssetRepresentationMode = .automatic
@Field
var cameraType: CameraType = .back
@Field
var allowsMultipleSelection: Bool = false
@Field
var selectionLimit: Int = UNLIMITED_SELECTION
@Field
var orderedSelection: Bool = false
}
internal enum PresentationStyle: String, EnumArgument {
case fullScreen
case pageSheet
case formSheet
case currentContext
case overFullScreen
case overCurrentContext
case popover
case none
case automatic
func toPresentationStyle() -> UIModalPresentationStyle {
switch self {
case .fullScreen:
return .fullScreen
case .pageSheet:
return .pageSheet
case .formSheet:
return .formSheet
case .currentContext:
return .currentContext
case .overFullScreen:
return .overFullScreen
case .overCurrentContext:
return .overCurrentContext
case .popover:
return .popover
case .none:
return .none
case .automatic:
if #available(iOS 13.0, *) {
return .automatic
}
// default prior iOS 13
return .fullScreen
}
}
}
internal enum PreferredAssetRepresentationMode: String, EnumArgument {
case automatic
case compatible
case current
@available(iOS 14.0, *)
func toAssetRepresentationMode() -> PHPickerConfiguration.AssetRepresentationMode {
switch self {
case .automatic:
return .automatic
case .compatible:
return .compatible
case .current:
return .current
}
}
}
internal enum VideoQuality: Int, EnumArgument {
case typeHigh = 0
case typeMedium = 1
case typeLow = 2
case type640x480 = 3
case typeIFrame1280x720 = 4
case typeIFrame960x540 = 5
func toQualityType() -> UIImagePickerController.QualityType {
switch self {
case .typeHigh:
return .typeHigh
case .typeMedium:
return .typeMedium
case .typeLow:
return .typeLow
case .type640x480:
return .type640x480
case .typeIFrame1280x720:
return .typeIFrame1280x720
case .typeIFrame960x540:
return .typeIFrame960x540
}
}
}
internal enum MediaType: String, EnumArgument {
case all = "All"
case videos = "Videos"
case images = "Images"
func toArray() -> [String] {
switch self {
case .images:
return [kUTTypeImage as String]
case .videos:
return [kUTTypeMovie as String]
case .all:
return [kUTTypeImage as String, kUTTypeMovie as String]
}
}
func requiresMicrophonePermission() -> Bool {
switch self {
case .images:
return false
case .videos:
return true
case .all:
return true
}
}
@available(iOS 14, *)
func toPickerFilter() -> PHPickerFilter {
// TODO: (barthap) Maybe add support for live photos
switch self {
case .images:
return .images
case .videos:
return .videos
case .all:
return .any(of: [.images, .videos])
}
}
}
internal enum VideoExportPreset: Int, EnumArgument {
case passthrough = 0
case lowQuality = 1
case mediumQuality = 2
case highestQuality = 3
case h264_640x480 = 4
case h264_960x540 = 5
case h264_1280x720 = 6
case h264_1920x1080 = 7
case h264_3840x2160 = 8
case hevc_1920x1080 = 9
case hevc_3840_2160 = 10
func toAVAssetExportPreset() -> String {
switch self {
case .passthrough:
return AVAssetExportPresetPassthrough
case .lowQuality:
return AVAssetExportPresetLowQuality
case .mediumQuality:
return AVAssetExportPresetMediumQuality
case .highestQuality:
return AVAssetExportPresetHighestQuality
case .h264_640x480:
return AVAssetExportPreset640x480
case .h264_960x540:
return AVAssetExportPreset960x540
case .h264_1280x720:
return AVAssetExportPreset1280x720
case .h264_1920x1080:
return AVAssetExportPreset1920x1080
case .h264_3840x2160:
return AVAssetExportPreset3840x2160
case .hevc_1920x1080:
return AVAssetExportPresetHEVC1920x1080
case .hevc_3840_2160:
return AVAssetExportPresetHEVC3840x2160
}
}
}
internal enum CameraType: String, EnumArgument {
case back
case front
}

View File

@@ -0,0 +1,136 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import Photos
import ExpoModulesCore
public class CameraPermissionRequester: NSObject, EXPermissionsRequester {
static public func permissionType() -> String {
return "camera"
}
public func requestPermissions(resolver resolve: @escaping EXPromiseResolveBlock, rejecter reject: EXPromiseRejectBlock) {
AVCaptureDevice.requestAccess(for: AVMediaType.video) { [weak self] _ in
resolve(self?.getPermissions())
}
}
public func getPermissions() -> [AnyHashable: Any] {
var systemStatus: AVAuthorizationStatus
var status: EXPermissionStatus
let cameraUsageDescription = Bundle.main.object(forInfoDictionaryKey: "NSCameraUsageDescription")
if cameraUsageDescription == nil {
EXFatal(EXErrorWithMessage("""
This app is missing 'NSCameraUsageDescription', video services will fail. \
Ensure this key exists in the app's Info.plist
"""))
systemStatus = AVAuthorizationStatus.denied
} else {
systemStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.video)
}
switch systemStatus {
case .authorized:
status = EXPermissionStatusGranted
case .restricted,
.denied:
status = EXPermissionStatusDenied
case .notDetermined:
fallthrough
@unknown default:
status = EXPermissionStatusUndetermined
}
return [
"status": status.rawValue
]
}
}
public class MediaLibraryPermissionRequester: DefaultMediaLibraryPermissionRequester,
EXPermissionsRequester {
public static func permissionType() -> String {
return "mediaLibrary"
}
}
public class MediaLibraryWriteOnlyPermissionRequester: DefaultMediaLibraryPermissionRequester,
EXPermissionsRequester {
public static func permissionType() -> String {
return "mediaLibraryWriteOnly"
}
@available(iOS 14, *)
override internal func accessLevel() -> PHAccessLevel {
return PHAccessLevel.addOnly
}
}
// MARK: - Permission requesters shared implementation extracted to an extension (mixin pattern)
/**
* Dummy class just to prevent extending NSObject publicly/globally.
*/
public class DefaultMediaLibraryPermissionRequester: NSObject {}
/**
* This extension is adding default implmentation for EXPermissionsRequester that can be shared by many classe.
* In Swift language you cannot override static methods in subclasses, so you cannot subclass any already implemented
* PermissionRequester as instances of this class are registered by the unique name coming from `static func permissionType()`.
* To prevent repeating the similar code for every MediaLibrary PermissionRequester (the only differences so far are
* aforementioned permissionType and accessLevel, while the latter can be easily overritten) I've extracted the code
* to this extension. I'm using as a mixin that implements major part of EXPermissionsRequester protocol.
*/
extension DefaultMediaLibraryPermissionRequester {
@objc
public func requestPermissions(resolver resolve: @escaping EXPromiseResolveBlock, rejecter reject: EXPromiseRejectBlock) {
let authorizationHandler = { [weak self] (_: PHAuthorizationStatus) in
resolve(self?.getPermissions())
}
if #available(iOS 14.0, *) {
PHPhotoLibrary.requestAuthorization(for: self.accessLevel(), handler: authorizationHandler)
} else {
PHPhotoLibrary.requestAuthorization(authorizationHandler)
}
}
@objc
public func getPermissions() -> [AnyHashable: Any] {
var authorizationStatus: PHAuthorizationStatus
if #available(iOS 14.0, *) {
authorizationStatus = PHPhotoLibrary.authorizationStatus(for: self.accessLevel())
} else {
authorizationStatus = PHPhotoLibrary.authorizationStatus()
}
var status: EXPermissionStatus
var scope: String
switch authorizationStatus {
case .authorized:
status = EXPermissionStatusGranted
scope = "all"
case .limited:
status = EXPermissionStatusGranted
scope = "limited"
case .denied, .restricted:
status = EXPermissionStatusDenied
scope = "none"
case .notDetermined:
fallthrough
@unknown default:
status = EXPermissionStatusUndetermined
scope = "none"
}
return [
"status": status.rawValue,
"accessPrivileges": scope
]
}
@available(iOS 14, *)
@objc
internal func accessLevel() -> PHAccessLevel {
return PHAccessLevel.readWrite
}
}

View File

@@ -0,0 +1,42 @@
// Copyright 2022-present 650 Industries. All rights reserved.
// swiftlint:disable redundant_optional_initialization
// Unfortunately, property wrappers must be initialized in those records, otherwise the memberwise initializer
// would require `Field<FieldType?>` as an argument instead of `FieldType?`.
// TODO: (@tsapeta) Figure out if we can fix that
import ExpoModulesCore
internal typealias ImagePickerResult = Result<ImagePickerResponse, Exception>
internal typealias SelectedMediaResult = Result<AssetInfo, Exception>
/**
Convenience alias, a dictionary representing EXIF data
*/
internal typealias ExifInfo = [String: Any]
/**
Represents a picker response.
*/
internal struct ImagePickerResponse: Record {
@Field var assets: [AssetInfo]? = nil
@Field var canceled: Bool = true
}
/**
Represents a single asset (image or video).
*/
internal struct AssetInfo: Record {
@Field var assetId: String? = nil
@Field var type: String = "image"
@Field var uri: String = ""
@Field var width: Double = 0
@Field var height: Double = 0
@Field var fileName: String? = nil
@Field var fileSize: Int? = nil
@Field var mimeType: String? = nil
@Field var base64: String? = nil
@Field var exif: ExifInfo? = nil
@Field var duration: Double? = nil
}

View File

@@ -0,0 +1,711 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import ExpoModulesCore
import MobileCoreServices
import Photos
import PhotosUI
internal struct MediaHandler {
internal weak var fileSystem: EXFileSystemInterface?
internal let options: ImagePickerOptions
internal func handleMedia(_ mediaInfo: MediaInfo, completion: @escaping (ImagePickerResult) -> Void) {
let mediaType: String? = mediaInfo[UIImagePickerController.InfoKey.mediaType] as? String
let imageType = kUTTypeImage as String
let videoType = kUTTypeMovie as String
switch mediaType {
case imageType: return handleImage(mediaInfo: mediaInfo, completion: completion)
case videoType: return handleVideo(mediaInfo: mediaInfo, completion: completion)
default: return completion(.failure(InvalidMediaTypeException(mediaType)))
}
}
@available(iOS 14, *)
internal func handleMultipleMedia(_ selection: [PHPickerResult], completion: @escaping (ImagePickerResult) -> Void) {
var results = [AssetInfo?](repeating: nil, count: selection.count)
let dispatchGroup = DispatchGroup()
let dispatchQueue = DispatchQueue(label: "expo.imagepicker.multipleMediaHandler")
let resultHandler = { (index: Int, result: SelectedMediaResult) -> Void in
switch result {
case .failure(let exception):
return completion(.failure(exception))
case .success(let mediaInfo):
dispatchQueue.async {
results[index] = mediaInfo
dispatchGroup.leave()
}
}
}
for (index, selectedItem) in selection.enumerated() {
let itemProvider = selectedItem.itemProvider
dispatchGroup.enter()
if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
handleImage(from: selectedItem, atIndex: index, completion: resultHandler)
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
handleVideo(from: selectedItem, atIndex: index, completion: resultHandler)
} else {
completion(.failure(InvalidMediaTypeException(itemProvider.registeredTypeIdentifiers.first)))
}
}
dispatchGroup.notify(queue: .main) {
completion(.success(
ImagePickerResponse(assets: results.compactMap({ $0 }), canceled: false)
))
}
}
// MARK: - Image
// TODO: convert to async/await syntax once we drop support for iOS 12
private func handleImage(mediaInfo: MediaInfo, completion: @escaping (ImagePickerResult) -> Void) {
do {
guard let image = ImageUtils.readImageFrom(mediaInfo: mediaInfo, shouldReadCroppedImage: options.allowsEditing) else {
return completion(.failure(FailedToReadImageException()))
}
let (imageData, fileExtension) = try ImageUtils.readDataAndFileExtension(image: image,
mediaInfo: mediaInfo,
options: options)
let targetUrl = try generateUrl(withFileExtension: fileExtension)
let mimeType = getMimeType(from: ".\(targetUrl.pathExtension)")
// no modification requested
let imageModified = options.allowsEditing || options.quality != nil
let fileWasCopied = !imageModified && ImageUtils.tryCopyingOriginalImageFrom(mediaInfo: mediaInfo, to: targetUrl)
if !fileWasCopied {
try ImageUtils.write(imageData: imageData, to: targetUrl)
}
// as calling this already requires media library permission, we can access it here
// if user gave limited permissions, in the worst case this will be null
let asset = mediaInfo[.phAsset] as? PHAsset
var fileName = asset?.value(forKey: "filename") as? String
// Extension will change to png when editing BMP files, reflect that change in fileName
if let unwrappedName = fileName {
fileName = replaceFileExtension(fileName: unwrappedName, targetExtension: fileExtension.lowercased())
}
let fileSize = getFileSize(from: targetUrl)
let base64 = try ImageUtils.optionallyReadBase64From(imageData: imageData,
orImageFileUrl: targetUrl,
tryReadingFile: fileWasCopied,
shouldReadBase64: self.options.base64)
ImageUtils.optionallyReadExifFrom(mediaInfo: mediaInfo, shouldReadExif: self.options.exif) { exif in
let imageInfo = AssetInfo(assetId: asset?.localIdentifier,
uri: targetUrl.absoluteString,
width: image.size.width,
height: image.size.height,
fileName: fileName,
fileSize: fileSize,
mimeType: mimeType,
base64: base64,
exif: exif)
let response = ImagePickerResponse(assets: [imageInfo], canceled: false)
completion(.success(response))
}
} catch let exception as Exception {
return completion(.failure(exception))
} catch {
return completion(.failure(UnexpectedException(error)))
}
}
@available(iOS 14, *)
private func handleImage(from selectedImage: PHPickerResult,
atIndex index: Int = -1,
completion: @escaping (Int, SelectedMediaResult) -> Void) {
let itemProvider = selectedImage.itemProvider
itemProvider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { rawData, error in
do {
guard error == nil,
let rawData = rawData,
let image = try? UIImage(data: rawData) else {
return completion(index, .failure(FailedToReadImageException().causedBy(error)))
}
let (imageData, fileExtension) = try ImageUtils.readDataAndFileExtension(image: image,
rawData: rawData,
itemProvider: itemProvider,
options: self.options)
let mimeType = getMimeType(from: fileExtension)
let targetUrl = try generateUrl(withFileExtension: fileExtension)
try ImageUtils.write(imageData: imageData, to: targetUrl)
let fileSize = getFileSize(from: targetUrl)
let fileName = itemProvider.suggestedName.map { $0 + fileExtension }
// We need to get EXIF from original image data, as it is being lost in UIImage
let exif = ImageUtils.optionallyReadExifFrom(data: rawData, shouldReadExif: self.options.exif)
let base64 = try ImageUtils.optionallyReadBase64From(imageData: imageData,
orImageFileUrl: targetUrl,
tryReadingFile: false,
shouldReadBase64: self.options.base64)
let imageInfo = AssetInfo(assetId: selectedImage.assetIdentifier,
uri: targetUrl.absoluteString,
width: image.size.width,
height: image.size.height,
fileName: fileName,
fileSize: fileSize,
mimeType: mimeType,
base64: base64,
exif: exif)
completion(index, .success(imageInfo))
} catch let exception as Exception {
return completion(index, .failure(exception))
} catch {
return completion(index, .failure(UnexpectedException(error)))
}
} // loadObject
}
private func getMimeType(from pathExtension: String) -> String? {
let filenameExtension = String(pathExtension.dropFirst())
if #available(iOS 14, *) {
return UTType(filenameExtension: filenameExtension)?.preferredMIMEType
}
if let uti = UTTypeCreatePreferredIdentifierForTag(
kUTTagClassFilenameExtension,
pathExtension as NSString, nil
)?.takeRetainedValue() {
if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
return mimetype as String
}
}
return nil
}
// MARK: - Video
// TODO: convert to async/await syntax once we drop support for iOS 12
func handleVideo(mediaInfo: MediaInfo, completion: (ImagePickerResult) -> Void) {
do {
guard let pickedVideoUrl = VideoUtils.readVideoUrlFrom(mediaInfo: mediaInfo) else {
return completion(.failure(FailedToReadVideoException()))
}
let targetUrl = try generateUrl(withFileExtension: ".mov")
try VideoUtils.tryCopyingVideo(at: pickedVideoUrl, to: targetUrl)
guard let dimensions = VideoUtils.readSizeFrom(url: targetUrl) else {
return completion(.failure(FailedToReadVideoSizeException()))
}
// If video was edited (the duration is affected) then read the duration from the original edited video.
// Otherwise read the duration from the target video file.
// TODO: (@bbarthec): inspect whether it makes sense to read duration from two different assets
let videoUrlToReadDurationFrom = self.options.allowsEditing ? pickedVideoUrl : targetUrl
let duration = VideoUtils.readDurationFrom(url: videoUrlToReadDurationFrom)
let asset = mediaInfo[.phAsset] as? PHAsset
let mimeType = getMimeType(from: ".\(targetUrl.pathExtension)")
let fileName = asset?.value(forKey: "filename") as? String
let fileSize = getFileSize(from: targetUrl)
let videoInfo = AssetInfo(assetId: asset?.localIdentifier,
type: "video",
uri: targetUrl.absoluteString,
width: dimensions.width,
height: dimensions.height,
fileName: fileName,
fileSize: fileSize,
mimeType: mimeType,
duration: duration)
completion(.success(ImagePickerResponse(assets: [videoInfo], canceled: false)))
} catch let exception as Exception {
return completion(.failure(exception))
} catch {
return completion(.failure(UnexpectedException(error)))
}
}
@available(iOS 14, *)
private func handleVideo(from selectedVideo: PHPickerResult,
atIndex index: Int = -1,
completion: @escaping (Int, SelectedMediaResult) -> Void) {
let itemProvider = selectedVideo.itemProvider
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { [self] url, error in
do {
guard error == nil,
let videoUrl = url as? URL else {
return completion(index, .failure(FailedToReadVideoException().causedBy(error)))
}
// In case of passthrough, we want original file extension, mp4 otherwise
// TODO: (barthap) Support other file extensions?
let transcodeFileType = AVFileType.mp4
let transcodeFileExtension = ".mp4"
let originalExtension = ".\(videoUrl.pathExtension)"
let mimeType = getMimeType(from: originalExtension)
// We need to copy the result into a place that we control, because the picker
// can remove the original file during conversion.
// Also, the transcoding may need a separate url - one of these will be used as a final result
let assetUrl = try generateUrl(withFileExtension: originalExtension)
let transcodedUrl = try generateUrl(withFileExtension: transcodeFileExtension)
try VideoUtils.tryCopyingVideo(at: videoUrl, to: assetUrl)
VideoUtils.transcodeVideoAsync(sourceAssetUrl: assetUrl,
destinationUrl: transcodedUrl,
outputFileType: transcodeFileType,
exportPreset: options.videoExportPreset) { result in
switch result {
case .failure(let exception):
return completion(index, .failure(exception))
case .success(let targetUrl):
let fileName = itemProvider.suggestedName.map { $0 + transcodeFileExtension }
let videoResult = buildVideoResult(for: targetUrl, withName: fileName, mimeType: mimeType, assetId: selectedVideo.assetIdentifier)
return completion(index, videoResult)
}
}
} catch let exception as Exception {
return completion(index, .failure(exception))
} catch {
return completion(index, .failure(UnexpectedException(error)))
}
}
}
// MARK: - utils
private func replaceFileExtension(fileName: String, targetExtension: String) -> String {
if !fileName.lowercased().hasSuffix(targetExtension.lowercased()) {
return deleteFileExtension(fileName: fileName) + targetExtension
}
return fileName
}
private func deleteFileExtension(fileName: String) -> String {
var components = fileName.components(separatedBy: ".")
guard components.count > 1 else {
return fileName
}
components.removeLast()
return components.joined(separator: ".")
}
private func generateUrl(withFileExtension: String) throws -> URL {
guard let fileSystem = self.fileSystem else {
throw FileSystemModuleNotFoundException()
}
let directory = fileSystem.cachesDirectory.appending(
fileSystem.cachesDirectory.hasSuffix("/") ? "" : "/" + "ImagePicker"
)
let path = fileSystem.generatePath(inDirectory: directory, withExtension: withFileExtension)
let url = URL(fileURLWithPath: path)
return url
}
private func buildVideoResult(for videoUrl: URL, withName fileName: String?, mimeType: String?, assetId: String?) -> SelectedMediaResult {
guard let size = VideoUtils.readSizeFrom(url: videoUrl) else {
return .failure(FailedToReadVideoSizeException())
}
let duration = VideoUtils.readDurationFrom(url: videoUrl)
let fileSize = getFileSize(from: videoUrl)
let result = AssetInfo(assetId: assetId,
type: "video",
uri: videoUrl.absoluteString,
width: size.width,
height: size.height,
fileName: fileName,
fileSize: fileSize,
mimeType: mimeType,
duration: duration)
return .success(result)
}
private func getFileSize(from fileUrl: URL) -> Int? {
do {
let resources = try fileUrl.resourceValues(forKeys: [.fileSizeKey])
return resources.fileSize
} catch {
log.error("Failed to get file size for \(fileUrl.absoluteString)")
return nil
}
}
}
private struct ImageUtils {
static func readImageFrom(mediaInfo: MediaInfo, shouldReadCroppedImage: Bool) -> UIImage? {
guard let originalImage = mediaInfo[.originalImage] as? UIImage,
let image = originalImage.fixOrientation()
else {
return nil
}
if !shouldReadCroppedImage {
return image
}
guard let cropRect = mediaInfo[.cropRect] as? CGRect,
let croppedImage = ImageUtils.crop(image: image, to: cropRect)
else {
return nil
}
return croppedImage
}
static func crop(image: UIImage, to: CGRect) -> UIImage? {
guard let cgImage = image.cgImage?.cropping(to: to) else {
return nil
}
return UIImage(cgImage: cgImage,
scale: image.scale,
orientation: image.imageOrientation)
}
static func readDataAndFileExtension(
image: UIImage,
mediaInfo: MediaInfo,
options: ImagePickerOptions
) throws -> (imageData: Data?, fileExtension: String) {
let compressionQuality = options.quality ?? DEFAULT_QUALITY
// nil when an image is picked from camera
let referenceUrl = mediaInfo[.referenceURL] as? URL
switch referenceUrl?.absoluteString {
case .some(let s) where s.contains("ext=PNG"):
let data = image.pngData()
return (data, ".png")
case .some(let s) where s.contains("ext=WEBP"):
if options.allowsEditing {
// switch to png if editing
let data = image.pngData()
return (data, ".png")
}
return (nil, ".webp")
case .some(let s) where s.contains("ext=BMP"):
if options.allowsEditing {
// switch to png if editing
let data = image.pngData()
return (data, ".png")
}
return (nil, ".bmp")
case .some(let s) where s.contains("ext=GIF"):
var rawData: Data?
if let imgUrl = mediaInfo[.imageURL] as? URL {
rawData = try? Data(contentsOf: imgUrl)
}
let inputData = rawData ?? image.jpegData(compressionQuality: compressionQuality)
let metadata = mediaInfo[.mediaMetadata] as? [String: Any]
let cropRect = options.allowsEditing ? mediaInfo[.cropRect] as? CGRect : nil
let gifData = try processGifData(inputData: inputData,
compressionQuality: options.quality,
initialMetadata: metadata,
cropRect: cropRect)
return (gifData, ".gif")
default:
let data = image.jpegData(compressionQuality: compressionQuality)
return (data, ".jpg")
}
}
@available(iOS 14, *)
static func readDataAndFileExtension(
image: UIImage,
rawData: Data,
itemProvider: NSItemProvider,
options: ImagePickerOptions
) throws -> (imageData: Data?, fileExtension: String) {
let compressionQuality = options.quality ?? DEFAULT_QUALITY
let preferredFormat = itemProvider.registeredTypeIdentifiers.first
switch preferredFormat {
case UTType.bmp.identifier:
if options.allowsEditing {
// switch to png if editing
let data = image.pngData()
return (data, ".png")
}
return (rawData, ".bmp")
case UTType.png.identifier:
let data = image.pngData()
return (data, ".png")
case UTType.webP.identifier:
if options.allowsEditing {
// switch to png if editing
let data = image.pngData()
return (data, ".png")
}
return (rawData, ".webp")
case UTType.gif.identifier:
let gifData = try processGifData(inputData: rawData,
compressionQuality: options.quality,
initialMetadata: nil)
return (gifData, ".gif")
default:
let data = image.jpegData(compressionQuality: compressionQuality)
return (data, ".jpg")
}
}
static func write(imageData: Data?, to: URL) throws {
do {
try imageData?.write(to: to, options: [.atomic])
} catch {
throw FailedToWriteImageException()
.causedBy(error)
}
}
/**
@returns `true` upon copying success and `false` otherwise
*/
static func tryCopyingOriginalImageFrom(mediaInfo: MediaInfo, to: URL) -> Bool {
guard let from = mediaInfo[.imageURL] as? URL else {
return false
}
do {
try FileManager.default.copyItem(atPath: from.path, toPath: to.path)
return true
} catch {
return false
}
}
/**
Reads base64 representation of the image data. If the data is `nil` fallbacks to reading the data from the url.
*/
static func optionallyReadBase64From(
imageData: Data?,
orImageFileUrl url: URL,
tryReadingFile: Bool,
shouldReadBase64: Bool
) throws -> String? {
if !shouldReadBase64 {
return nil
}
if tryReadingFile {
do {
let data = try Data(contentsOf: url)
return data.base64EncodedString()
} catch {
throw FailedToReadImageDataException()
.causedBy(error)
}
}
guard let data = imageData else {
throw FailedToReadImageDataForBase64Exception()
}
return data.base64EncodedString()
}
static func optionallyReadExifFrom(
mediaInfo: MediaInfo,
shouldReadExif: Bool,
completion: @escaping (_ result: ExifInfo?) -> Void
) {
if !shouldReadExif {
return completion(nil)
}
let metadata = mediaInfo[.mediaMetadata] as? [String: Any]
if metadata != nil {
let exif = ImageUtils.readExifFrom(imageMetadata: metadata!)
return completion(exif)
}
guard let imageUrl = mediaInfo[.referenceURL] as? URL else {
log.error("Could not fetch metadata for image")
return completion(nil)
}
let assets = PHAsset.fetchAssets(withALAssetURLs: [imageUrl], options: nil)
guard let asset = assets.firstObject else {
log.error("Could not fetch metadata for image '\(imageUrl.absoluteString)'.")
return completion(nil)
}
let options = PHContentEditingInputRequestOptions()
options.isNetworkAccessAllowed = true
asset.requestContentEditingInput(with: options) { input, _ in
guard let imageUrl = input?.fullSizeImageURL,
let properties = CIImage(contentsOf: imageUrl)?.properties
else {
log.error("Could not fetch metadata for '\(imageUrl.absoluteString)'.")
return completion(nil)
}
let exif = ImageUtils.readExifFrom(imageMetadata: properties)
return completion(exif)
}
}
static func optionallyReadExifFrom(data: Data, shouldReadExif: Bool) -> ExifInfo? {
if shouldReadExif,
let cgImageSource = CGImageSourceCreateWithData(data as CFData, nil),
let properties = CGImageSourceCopyPropertiesAtIndex(cgImageSource, 0, nil) {
return ImageUtils.readExifFrom(imageMetadata: properties as! [String: Any])
}
return nil
}
static func readExifFrom(imageMetadata: [String: Any]) -> ExifInfo {
var exif: ExifInfo = imageMetadata[kCGImagePropertyExifDictionary as String] as? ExifInfo ?? [:]
// Copy ["{GPS}"]["<tag>"] to ["GPS<tag>"]
let gps = imageMetadata[kCGImagePropertyGPSDictionary as String] as? [String: Any]
if gps != nil {
gps!.forEach { key, value in
exif["GPS\(key)"] = value
}
}
// Inject orientation into exif
let orientationKey = kCGImagePropertyOrientation as String
let orientationValue = imageMetadata[orientationKey]
if orientationValue != nil {
exif[orientationKey] = orientationValue
}
return exif
}
static func processGifData(
inputData: Data?,
compressionQuality: Double?,
initialMetadata: [String: Any]?,
cropRect: CGRect? = nil
) throws -> Data? {
let quality = compressionQuality ?? MAXIMUM_QUALITY
// for uncropped, maximum quality image we can just pass through the raw data
if cropRect == nil && quality >= MAXIMUM_QUALITY {
return inputData
}
guard let sourceData = inputData,
let imageSource = CGImageSourceCreateWithData(sourceData as CFData, nil)
else {
throw FailedToReadImageException()
}
let gifProperties = CGImageSourceCopyProperties(imageSource, nil) as? [String: Any]
let frameCount = CGImageSourceGetCount(imageSource)
let destinationData = NSMutableData()
guard let imageDestination = CGImageDestinationCreateWithData(destinationData, kUTTypeGIF, frameCount, nil)
else {
throw FailedToCreateGifException()
}
let gifMetadata = initialMetadata ?? gifProperties
CGImageDestinationSetProperties(imageDestination, gifMetadata as CFDictionary?)
for frameIndex in 0 ..< frameCount {
guard var cgImage = CGImageSourceCreateImageAtIndex(imageSource, frameIndex, nil),
var frameProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, frameIndex, nil) as? [String: Any]
else {
throw FailedToCreateGifException()
}
if cropRect != nil {
cgImage = cgImage.cropping(to: cropRect!)!
}
if quality != nil {
frameProperties[kCGImageDestinationLossyCompressionQuality as String] = quality
}
CGImageDestinationAddImage(imageDestination, cgImage, frameProperties as CFDictionary)
}
if !CGImageDestinationFinalize(imageDestination) {
throw FailedToExportGifException()
}
return destinationData as Data
}
}
private struct VideoUtils {
static func tryCopyingVideo(at: URL, to: URL) throws {
do {
// we copy the file as `moveItem(at:,to:)` throws an error in iOS 13 due to missing permissions
try FileManager.default.copyItem(at: at, to: to)
} catch {
throw FailedToPickVideoException()
.causedBy(error)
}
}
/**
@returns duration in milliseconds
*/
static func readDurationFrom(url: URL) -> Double {
let asset = AVURLAsset(url: url)
return Double(asset.duration.value) / Double(asset.duration.timescale) * 1_000
}
static func readSizeFrom(url: URL) -> CGSize? {
let asset = AVURLAsset(url: url)
guard let assetTrack = asset.tracks(withMediaType: .video).first else {
return nil
}
// The video could be rotated and the resulting transform can result in a negative width/height.
let size = assetTrack.naturalSize.applying(assetTrack.preferredTransform)
return CGSize(width: abs(size.width), height: abs(size.height))
}
static func readVideoUrlFrom(mediaInfo: MediaInfo) -> URL? {
return mediaInfo[.mediaURL] as? URL
?? mediaInfo[.referenceURL] as? URL
}
/**
Asynchronously transcodes asset provided as `sourceAssetUrl` according to `exportPreset`.
Result URL is returned to the `completion` closure.
Transcoded video is saved at `destinationUrl`, unless `exportPreset` is set to `passthrough`.
In this case, `sourceAssetUrl` is returned.
*/
static func transcodeVideoAsync(sourceAssetUrl: URL,
destinationUrl: URL,
outputFileType: AVFileType,
exportPreset: VideoExportPreset,
completion: @escaping (Result<URL, Exception>) -> Void) {
if case .passthrough = exportPreset {
return completion(.success((sourceAssetUrl)))
}
let asset = AVURLAsset(url: sourceAssetUrl)
let preset = exportPreset.toAVAssetExportPreset()
AVAssetExportSession.determineCompatibility(ofExportPreset: preset,
with: asset,
outputFileType: outputFileType) { canBeTranscoded in
guard canBeTranscoded else {
return completion(.failure(UnsupportedVideoExportPresetException(preset.description)))
}
guard let exportSession = AVAssetExportSession(asset: asset,
presetName: preset) else {
return completion(.failure(FailedToTranscodeVideoException()))
}
exportSession.outputFileType = outputFileType
exportSession.outputURL = destinationUrl
exportSession.exportAsynchronously {
switch exportSession.status {
case .failed:
let error = exportSession.error
completion(.failure(FailedToTranscodeVideoException().causedBy(error)))
default:
completion(.success((destinationUrl)))
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
// Copyright 2022-present 650 Industries. All rights reserved.
/**
Since iOS 11, launching ImagePicker with `allowsEditing` option makes cropping rectangle
slightly moved upwards, because of StatusBar visibility.
Hiding StatusBar during picking process solves the displacement issue.
See https://forums.developer.apple.com/thread/98274
*/
internal class StatusBarVisibilityController {
private var shouldRestoreStatusBarVisibility = false
func maybePreserveVisibilityAndHideStatusBar(_ shouldHideStatusBar: Bool) {
guard shouldHideStatusBar && !UIApplication.shared.isStatusBarHidden else {
return
}
shouldRestoreStatusBarVisibility = true
setStatusBarHidden(true)
}
func maybeRestoreStatusBarVisibility() {
guard shouldRestoreStatusBarVisibility else {
return
}
shouldRestoreStatusBarVisibility = false
setStatusBarHidden(false)
}
/**
Calling -[UIApplication setStatusBarHidden:withAnimation:] triggers a warning
that should be suppressable with -Wdeprecated-declarations, but is not.
The warning suggests to use -[UIViewController prefersStatusBarHidden].
Unfortunately until we stop presenting view controllers on detached VCs
the setting doesn't have any effect and we need to set status bar like that.
*/
private func setStatusBarHidden(_ hidden: Bool) {
let selector = NSSelectorFromString("setStatusBarHidden:withAnimation:")
UIApplication.shared.perform(selector, with: hidden, with: false)
// TODO: (@bbarthec) below is possible alternative
// let obj = X()
// let sel = #selector(obj.sayHiTo)
// let meth = class_getInstanceMethod(object_getClass(obj), sel)
// let imp = method_getImplementation(meth)
//
// typealias ClosureType = @convention(c) (AnyObject, Selector, String) -> Void
// let sayHiTo : ClosureType = unsafeBitCast(imp, ClosureType.self)
// sayHiTo(obj, sel, "Fabio")
// prints "Hello Fabio!"
}
}

View File

@@ -0,0 +1,82 @@
// Copyright 2022-present 650 Industries. All rights reserved.
import UIKit
extension UIImage {
func fixOrientation() -> UIImage? {
if self.imageOrientation == UIImage.Orientation.up {
return self
}
var transform = CGAffineTransform.identity
// rotation
switch self.imageOrientation {
case .down,
.downMirrored:
transform = transform
.translatedBy(x: self.size.width, y: self.size.height)
.rotated(by: .pi)
case .left,
.leftMirrored:
transform = transform
.translatedBy(x: self.size.width, y: 0)
.rotated(by: .pi / 2)
case .right,
.rightMirrored:
transform = transform
.translatedBy(x: 0, y: self.size.height)
.rotated(by: -.pi / 2)
default:
break
}
// mirroring
switch self.imageOrientation {
case .upMirrored,
.downMirrored:
transform = transform
.translatedBy(x: self.size.width, y: 0)
.scaledBy(x: -1, y: 1)
case .leftMirrored,
.rightMirrored:
transform = transform
.translatedBy(x: self.size.height, y: 0)
.scaledBy(x: -1, y: 1)
default:
break
}
guard let cgImage = self.cgImage,
let colorSpace = cgImage.colorSpace,
let ctx = CGContext(data: nil,
width: Int(self.size.width),
height: Int(self.size.height),
bitsPerComponent: cgImage.bitsPerComponent,
bytesPerRow: 0,
space: colorSpace,
bitmapInfo: cgImage.bitmapInfo.rawValue)
else {
return nil
}
ctx.concatenate(transform)
switch self.imageOrientation {
case .left,
.leftMirrored,
.right,
.rightMirrored:
ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: self.size.height, height: self.size.width))
default:
ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
}
guard let resultCgImage = ctx.makeImage() else {
return nil
}
let result = UIImage(cgImage: resultCgImage)
return result
}
}

View File

@@ -0,0 +1,57 @@
// Copyright 2016-present 650 Industries. All rights reserved.
extension UIImagePickerController {
func fixCannotMoveEditingBox() {
if let cropView = cropView,
let scrollView = scrollView,
scrollView.contentOffset.y == 0 {
let top = cropView.frame.minY + self.view.safeAreaInsets.top
let bottom = scrollView.frame.height - cropView.frame.height - top
scrollView.contentInset = UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0)
var offset: CGFloat = 0
if scrollView.contentSize.height > scrollView.contentSize.width {
offset = 0.5 * (scrollView.contentSize.height - scrollView.contentSize.width)
}
scrollView.contentOffset = CGPoint(x: 0, y: -top + offset)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.fixCannotMoveEditingBox()
}
}
var cropView: UIView? {
return findCropView(from: self.view)
}
var scrollView: UIScrollView? {
return findScrollView(from: self.view)
}
func findCropView(from view: UIView) -> UIView? {
let width = UIScreen.main.bounds.width
let size = view.bounds.size
if width == size.height, width == size.height {
return view
}
for view in view.subviews {
if let cropView = findCropView(from: view) {
return cropView
}
}
return nil
}
func findScrollView(from view: UIView) -> UIScrollView? {
if let scrollView = view as? UIScrollView {
return scrollView
}
for view in view.subviews {
if let scrollView = findScrollView(from: view) {
return scrollView
}
}
return nil
}
}

View File

@@ -0,0 +1,48 @@
{
"name": "expo-image-picker",
"version": "15.0.7",
"description": "Provides access to the system's UI for selecting images and videos from the phone's library or taking a photo with the camera.",
"main": "build/ImagePicker.js",
"types": "build/ImagePicker.d.ts",
"sideEffects": false,
"scripts": {
"build": "expo-module build",
"clean": "expo-module clean",
"lint": "expo-module lint",
"test": "expo-module test",
"prepare": "expo-module prepare",
"prepublishOnly": "expo-module prepublishOnly",
"expo-module": "expo-module"
},
"keywords": [
"react-native",
"expo",
"image",
"picker",
"image-picker"
],
"repository": {
"type": "git",
"url": "https://github.com/expo/expo.git",
"directory": "packages/expo-image-picker"
},
"bugs": {
"url": "https://github.com/expo/expo/issues"
},
"author": "650 Industries, Inc.",
"license": "MIT",
"homepage": "https://docs.expo.dev/versions/latest/sdk/imagepicker/",
"jest": {
"preset": "expo-module-scripts"
},
"dependencies": {
"expo-image-loader": "~4.7.0"
},
"devDependencies": {
"expo-module-scripts": "^3.0.0"
},
"peerDependencies": {
"expo": "*"
},
"gitHead": "c37efa4f4ebe6137c34eb5813843df7015f3c22f"
}

View File

@@ -0,0 +1,9 @@
import { ConfigPlugin } from '@expo/config-plugins';
type Props = {
photosPermission?: string | false;
cameraPermission?: string | false;
microphonePermission?: string | false;
};
export declare const withAndroidImagePickerPermissions: ConfigPlugin<Props | void>;
declare const _default: ConfigPlugin<void | Props>;
export default _default;

View File

@@ -0,0 +1,47 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.withAndroidImagePickerPermissions = void 0;
const config_plugins_1 = require("@expo/config-plugins");
const pkg = require('expo-image-picker/package.json');
const CAMERA_USAGE = 'Allow $(PRODUCT_NAME) to access your camera';
const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone';
const READ_PHOTOS_USAGE = 'Allow $(PRODUCT_NAME) to access your photos';
const withAndroidImagePickerPermissions = (config, { cameraPermission, microphonePermission } = {}) => {
if (microphonePermission !== false) {
config = config_plugins_1.AndroidConfig.Permissions.withPermissions(config, ['android.permission.RECORD_AUDIO']);
}
// If the user manually sets any of the permissions to `false`, then we should block the permissions to ensure no
// package can add them.
config = config_plugins_1.AndroidConfig.Permissions.withBlockedPermissions(config, [
microphonePermission === false && 'android.permission.RECORD_AUDIO',
cameraPermission === false && 'android.permission.CAMERA',
].filter(Boolean));
// NOTE(EvanBacon): It's unclear if we should block the WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE permissions since
// they're used for many other things besides image picker.
return config;
};
exports.withAndroidImagePickerPermissions = withAndroidImagePickerPermissions;
const withImagePicker = (config, { photosPermission, cameraPermission, microphonePermission } = {}) => {
config_plugins_1.IOSConfig.Permissions.createPermissionsPlugin({
NSPhotoLibraryUsageDescription: READ_PHOTOS_USAGE,
NSCameraUsageDescription: CAMERA_USAGE,
NSMicrophoneUsageDescription: MICROPHONE_USAGE,
})(config, {
NSPhotoLibraryUsageDescription: photosPermission,
NSCameraUsageDescription: cameraPermission,
NSMicrophoneUsageDescription: microphonePermission,
});
if (microphonePermission !== false) {
config = config_plugins_1.AndroidConfig.Permissions.withPermissions(config, ['android.permission.RECORD_AUDIO']);
}
// If the user manually sets any of the permissions to `false`, then we should block the permissions to ensure no
// package can add them.
config = config_plugins_1.AndroidConfig.Permissions.withBlockedPermissions(config, [
microphonePermission === false && 'android.permission.RECORD_AUDIO',
cameraPermission === false && 'android.permission.CAMERA',
].filter(Boolean));
// NOTE(EvanBacon): It's unclear if we should block the WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE permissions since
// they're used for many other things besides image picker.
return config;
};
exports.default = (0, config_plugins_1.createRunOncePlugin)(withImagePicker, pkg.name, pkg.version);

View File

@@ -0,0 +1 @@
module.exports = require('expo-module-scripts/jest-preset-plugin');

View File

@@ -0,0 +1,73 @@
import { AndroidConfig, ConfigPlugin, createRunOncePlugin, IOSConfig } from '@expo/config-plugins';
const pkg = require('expo-image-picker/package.json');
const CAMERA_USAGE = 'Allow $(PRODUCT_NAME) to access your camera';
const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone';
const READ_PHOTOS_USAGE = 'Allow $(PRODUCT_NAME) to access your photos';
type Props = {
photosPermission?: string | false;
cameraPermission?: string | false;
microphonePermission?: string | false;
};
export const withAndroidImagePickerPermissions: ConfigPlugin<Props | void> = (
config,
{ cameraPermission, microphonePermission } = {}
) => {
if (microphonePermission !== false) {
config = AndroidConfig.Permissions.withPermissions(config, ['android.permission.RECORD_AUDIO']);
}
// If the user manually sets any of the permissions to `false`, then we should block the permissions to ensure no
// package can add them.
config = AndroidConfig.Permissions.withBlockedPermissions(
config,
[
microphonePermission === false && 'android.permission.RECORD_AUDIO',
cameraPermission === false && 'android.permission.CAMERA',
].filter(Boolean) as string[]
);
// NOTE(EvanBacon): It's unclear if we should block the WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE permissions since
// they're used for many other things besides image picker.
return config;
};
const withImagePicker: ConfigPlugin<Props | void> = (
config,
{ photosPermission, cameraPermission, microphonePermission } = {}
) => {
IOSConfig.Permissions.createPermissionsPlugin({
NSPhotoLibraryUsageDescription: READ_PHOTOS_USAGE,
NSCameraUsageDescription: CAMERA_USAGE,
NSMicrophoneUsageDescription: MICROPHONE_USAGE,
})(config, {
NSPhotoLibraryUsageDescription: photosPermission,
NSCameraUsageDescription: cameraPermission,
NSMicrophoneUsageDescription: microphonePermission,
});
if (microphonePermission !== false) {
config = AndroidConfig.Permissions.withPermissions(config, ['android.permission.RECORD_AUDIO']);
}
// If the user manually sets any of the permissions to `false`, then we should block the permissions to ensure no
// package can add them.
config = AndroidConfig.Permissions.withBlockedPermissions(
config,
[
microphonePermission === false && 'android.permission.RECORD_AUDIO',
cameraPermission === false && 'android.permission.CAMERA',
].filter(Boolean) as string[]
);
// NOTE(EvanBacon): It's unclear if we should block the WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE permissions since
// they're used for many other things besides image picker.
return config;
};
export default createRunOncePlugin(withImagePicker, pkg.name, pkg.version);

View File

@@ -0,0 +1,9 @@
{
"extends": "expo-module-scripts/tsconfig.plugin",
"compilerOptions": {
"outDir": "build",
"rootDir": "src"
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}

View File

@@ -0,0 +1,2 @@
import { requireNativeModule } from 'expo-modules-core';
export default requireNativeModule('ExponentImagePicker');

View File

@@ -0,0 +1,159 @@
import { PermissionResponse, PermissionStatus, Platform } from 'expo-modules-core';
import {
ImagePickerAsset,
ImagePickerResult,
MediaTypeOptions,
OpenFileBrowserOptions,
} from './ImagePicker.types';
const MediaTypeInput = {
[MediaTypeOptions.All]: 'video/mp4,video/quicktime,video/x-m4v,video/*,image/*',
[MediaTypeOptions.Images]: 'image/*',
[MediaTypeOptions.Videos]: 'video/mp4,video/quicktime,video/x-m4v,video/*',
};
export default {
async launchImageLibraryAsync({
mediaTypes = MediaTypeOptions.Images,
allowsMultipleSelection = false,
base64 = false,
}): Promise<ImagePickerResult> {
// SSR guard
if (!Platform.isDOMAvailable) {
return { canceled: true, assets: null };
}
return await openFileBrowserAsync({
mediaTypes,
allowsMultipleSelection,
base64,
});
},
async launchCameraAsync({
mediaTypes = MediaTypeOptions.Images,
allowsMultipleSelection = false,
base64 = false,
}): Promise<ImagePickerResult> {
// SSR guard
if (!Platform.isDOMAvailable) {
return { canceled: true, assets: null };
}
return await openFileBrowserAsync({
mediaTypes,
allowsMultipleSelection,
capture: true,
base64,
});
},
/*
* Delegate to expo-permissions to request camera permissions
*/
async getCameraPermissionsAsync() {
return permissionGrantedResponse();
},
async requestCameraPermissionsAsync() {
return permissionGrantedResponse();
},
/*
* Camera roll permissions don't need to be requested on web, so we always
* respond with granted.
*/
async getMediaLibraryPermissionsAsync(_writeOnly: boolean) {
return permissionGrantedResponse();
},
async requestMediaLibraryPermissionsAsync(_writeOnly: boolean): Promise<PermissionResponse> {
return permissionGrantedResponse();
},
};
function permissionGrantedResponse(): PermissionResponse {
return {
status: PermissionStatus.GRANTED,
expires: 'never',
granted: true,
canAskAgain: true,
};
}
function openFileBrowserAsync({
mediaTypes,
capture = false,
allowsMultipleSelection = false,
base64,
}: OpenFileBrowserOptions): Promise<ImagePickerResult> {
const mediaTypeFormat = MediaTypeInput[mediaTypes];
const input = document.createElement('input');
input.style.display = 'none';
input.setAttribute('type', 'file');
input.setAttribute('accept', mediaTypeFormat);
input.setAttribute('id', String(Math.random()));
if (allowsMultipleSelection) {
input.setAttribute('multiple', 'multiple');
}
if (capture) {
input.setAttribute('capture', 'camera');
}
document.body.appendChild(input);
return new Promise((resolve) => {
input.addEventListener('change', async () => {
if (input.files) {
const files = allowsMultipleSelection ? input.files : [input.files[0]];
const assets: ImagePickerAsset[] = await Promise.all(
Array.from(files).map((file) => readFile(file, { base64 }))
);
resolve({ canceled: false, assets });
} else {
resolve({ canceled: true, assets: null });
}
document.body.removeChild(input);
});
const event = new MouseEvent('click');
input.dispatchEvent(event);
});
}
function readFile(targetFile: File, options: { base64: boolean }): Promise<ImagePickerAsset> {
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 as any).result;
const returnRaw = () => resolve({ uri, 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,
mimeType: targetFile.type,
fileName: targetFile.name,
// 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.base64 && { base64: uri.substr(uri.indexOf(',') + 1) }),
});
};
image.onerror = () => returnRaw();
} else {
returnRaw();
}
};
reader.readAsDataURL(targetFile);
});
}

View File

@@ -0,0 +1,215 @@
import {
PermissionStatus,
PermissionExpiration,
PermissionHookOptions,
PermissionResponse,
createPermissionHook,
UnavailabilityError,
CodedError,
} from 'expo-modules-core';
import ExponentImagePicker from './ExponentImagePicker';
import {
CameraPermissionResponse,
MediaLibraryPermissionResponse,
ImagePickerResult,
ImagePickerErrorResult,
ImagePickerOptions,
} from './ImagePicker.types';
function validateOptions(options: ImagePickerOptions) {
const { aspect, quality, videoMaxDuration } = options;
if (aspect != null) {
const [x, y] = aspect;
if (x <= 0 || y <= 0) {
throw new CodedError(
'ERR_INVALID_ARGUMENT',
`Invalid aspect ratio values ${x}:${y}. Provide positive numbers.`
);
}
}
if (quality && (quality < 0 || quality > 1)) {
throw new CodedError(
'ERR_INVALID_ARGUMENT',
`Invalid 'quality' value ${quality}. Provide a value between 0 and 1.`
);
}
if (videoMaxDuration && videoMaxDuration < 0) {
throw new CodedError(
'ERR_INVALID_ARGUMENT',
`Invalid 'videoMaxDuration' value ${videoMaxDuration}. Provide a non-negative number.`
);
}
return options;
}
// @needsAudit
/**
* Checks user's permissions for accessing camera.
* @return A promise that fulfills with an object of type [CameraPermissionResponse](#camerapermissionresponse).
*/
export async function getCameraPermissionsAsync(): Promise<CameraPermissionResponse> {
return ExponentImagePicker.getCameraPermissionsAsync();
}
// @needsAudit
/**
* Checks user's permissions for accessing photos.
* @param writeOnly Whether to request write or read and write permissions. Defaults to `false`
* @return A promise that fulfills with an object of type [MediaLibraryPermissionResponse](#medialibrarypermissionresponse).
*/
export async function getMediaLibraryPermissionsAsync(
writeOnly: boolean = false
): Promise<MediaLibraryPermissionResponse> {
return ExponentImagePicker.getMediaLibraryPermissionsAsync(writeOnly);
}
// @needsAudit
/**
* Asks the user to grant permissions for accessing camera. This does nothing on web because the
* browser camera is not used.
* @return A promise that fulfills with an object of type [CameraPermissionResponse](#camerarollpermissionresponse).
*/
export async function requestCameraPermissionsAsync(): Promise<CameraPermissionResponse> {
return ExponentImagePicker.requestCameraPermissionsAsync();
}
// @needsAudit
/**
* Asks the user to grant permissions for accessing user's photo. This method does nothing on web.
* @param writeOnly Whether to request write or read and write permissions. Defaults to `false`
* @return A promise that fulfills with an object of type [MediaLibraryPermissionResponse](#medialibrarypermissionresponse).
*/
export async function requestMediaLibraryPermissionsAsync(
writeOnly: boolean = false
): Promise<MediaLibraryPermissionResponse> {
const imagePickerMethod = ExponentImagePicker.requestMediaLibraryPermissionsAsync;
return imagePickerMethod(writeOnly);
}
// @needsAudit
/**
* Check or request permissions to access the media library.
* This uses both `requestMediaLibraryPermissionsAsync` and `getMediaLibraryPermissionsAsync` to interact with the permissions.
*
* @example
* ```ts
* const [status, requestPermission] = ImagePicker.useMediaLibraryPermissions();
* ```
*/
export const useMediaLibraryPermissions = createPermissionHook<
MediaLibraryPermissionResponse,
{ writeOnly?: boolean }
>({
// TODO(cedric): permission requesters should have an options param or a different requester
getMethod: (options) => getMediaLibraryPermissionsAsync(options?.writeOnly),
requestMethod: (options) => requestMediaLibraryPermissionsAsync(options?.writeOnly),
});
// @needsAudit
/**
* Check or request permissions to access the camera.
* This uses both `requestCameraPermissionsAsync` and `getCameraPermissionsAsync` to interact with the permissions.
*
* @example
* ```ts
* const [status, requestPermission] = ImagePicker.useCameraPermissions();
* ```
*/
export const useCameraPermissions = createPermissionHook({
getMethod: getCameraPermissionsAsync,
requestMethod: requestCameraPermissionsAsync,
});
// @needsAudit
/**
* Android system sometimes kills the `MainActivity` after the `ImagePicker` finishes. When this
* happens, we lost the data selected from the `ImagePicker`. However, you can retrieve the lost
* data by calling `getPendingResultAsync`. You can test this functionality by turning on
* `Don't keep activities` in the developer options.
* @return
* - **On Android:** a promise that resolves to an array of objects of exactly same type as in
* `ImagePicker.launchImageLibraryAsync` or `ImagePicker.launchCameraAsync` if the `ImagePicker`
* finished successfully. Otherwise, to the array of [`ImagePickerErrorResult`](#imagepickerimagepickererrorresult).
* - **On other platforms:** an empty array.
*/
export async function getPendingResultAsync(): Promise<
(ImagePickerResult | ImagePickerErrorResult)[]
> {
if (ExponentImagePicker.getPendingResultAsync) {
return ExponentImagePicker.getPendingResultAsync();
}
return [];
}
// @needsAudit
/**
* Display the system UI for taking a photo with the camera. Requires `Permissions.CAMERA`.
* On Android and iOS 10 `Permissions.CAMERA_ROLL` is also required. On mobile web, this must be
* called immediately in a user interaction like a button press, otherwise the browser will block
* the request without a warning.
* > **Note:** Make sure that you handle `MainActivity` destruction on **Android**. See [ImagePicker.getPendingResultAsync](#imagepickergetpendingresultasync).
* > **Notes for Web:** The system UI can only be shown after user activation (e.g. a `Button` press).
* Therefore, calling `launchCameraAsync` in `componentDidMount`, for example, will **not** work as
* intended. The `cancelled` event will not be returned in the browser due to platform restrictions
* and inconsistencies across browsers.
* @param options An `ImagePickerOptions` object.
* @return A promise that resolves to an object with `canceled` and `assets` fields.
* When the user canceled the action the `assets` is always `null`, otherwise it's an array of
* the selected media assets which have a form of [`ImagePickerAsset`](#imagepickerasset).
*/
export async function launchCameraAsync(
options: ImagePickerOptions = {}
): Promise<ImagePickerResult> {
if (!ExponentImagePicker.launchCameraAsync) {
throw new UnavailabilityError('ImagePicker', 'launchCameraAsync');
}
return await ExponentImagePicker.launchCameraAsync(validateOptions(options));
}
// @needsAudit
/**
* Display the system UI for choosing an image or a video from the phone's library.
* Requires `Permissions.MEDIA_LIBRARY` on iOS 10 only. On mobile web, this must be called
* immediately in a user interaction like a button press, otherwise the browser will block the
* request without a warning.
*
* **Animated GIFs support:** On Android, if the selected image is an animated GIF, the result image will be an
* animated GIF too if and only if `quality` is explicitly set to `1.0` and `allowsEditing` is set to `false`.
* Otherwise compression and/or cropper will pick the first frame of the GIF and return it as the
* result (on Android the result will be a PNG). On iOS, both quality and cropping are supported.
*
* > **Notes for Web:** The system UI can only be shown after user activation (e.g. a `Button` press).
* Therefore, calling `launchImageLibraryAsync` in `componentDidMount`, for example, will **not**
* work as intended. The `cancelled` event will not be returned in the browser due to platform
* restrictions and inconsistencies across browsers.
* @param options An object extended by [`ImagePickerOptions`](#imagepickeroptions).
* @return A promise that resolves to an object with `canceled` and `assets` fields.
* When the user canceled the action the `assets` is always `null`, otherwise it's an array of
* the selected media assets which have a form of [`ImagePickerAsset`](#imagepickerasset).
*/
export async function launchImageLibraryAsync(
options?: ImagePickerOptions
): Promise<ImagePickerResult> {
if (!ExponentImagePicker.launchImageLibraryAsync) {
throw new UnavailabilityError('ImagePicker', 'launchImageLibraryAsync');
}
if (options?.allowsEditing && options.allowsMultipleSelection) {
console.warn(
'[expo-image-picker] `allowsEditing` is not supported when `allowsMultipleSelection` is enabled and will be ignored.' +
"Disable either 'allowsEditing' or 'allowsMultipleSelection' in 'launchImageLibraryAsync' " +
'to fix this warning.'
);
}
return await ExponentImagePicker.launchImageLibraryAsync(options ?? {});
}
export * from './ImagePicker.types';
export type { PermissionExpiration, PermissionHookOptions, PermissionResponse };
export { PermissionStatus };

View File

@@ -0,0 +1,513 @@
import { PermissionResponse } from 'expo-modules-core';
// @needsAudit
/**
* Alias for `PermissionResponse` type exported by `expo-modules-core`.
*/
export type CameraPermissionResponse = PermissionResponse;
// @needsAudit
/**
* Extends `PermissionResponse` type exported by `expo-modules-core`, containing additional iOS-specific field.
*/
export type MediaLibraryPermissionResponse = PermissionResponse & {
/**
* Indicates if your app has access to the whole or only part of the photo library. Possible values are:
* - `'all'` if the user granted your app access to the whole photo library
* - `'limited'` if the user granted your app access only to selected photos (only available on Android API 34+ and iOS 14.0+)
* - `'none'` if user denied or hasn't yet granted the permission
*/
accessPrivileges?: 'all' | 'limited' | 'none';
};
// @needsAudit
export enum MediaTypeOptions {
/**
* Images and videos.
*/
All = 'All',
/**
* Only videos.
*/
Videos = 'Videos',
/**
* Only images.
*/
Images = 'Images',
}
// @needsAudit
export enum VideoExportPreset {
/**
* Resolution: __Unchanged__ •
* Video compression: __None__ •
* Audio compression: __None__
*/
Passthrough = 0,
/**
* Resolution: __Depends on the device__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
LowQuality = 1,
/**
* Resolution: __Depends on the device__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
MediumQuality = 2,
/**
* Resolution: __Depends on the device__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
HighestQuality = 3,
/**
* Resolution: __640 × 480__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
H264_640x480 = 4,
/**
* Resolution: __960 × 540__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
H264_960x540 = 5,
/**
* Resolution: __1280 × 720__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
H264_1280x720 = 6,
/**
* Resolution: __1920 × 1080__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
H264_1920x1080 = 7,
/**
* Resolution: __3840 × 2160__ •
* Video compression: __H.264__ •
* Audio compression: __AAC__
*/
H264_3840x2160 = 8,
/**
* Resolution: __1920 × 1080__ •
* Video compression: __HEVC__ •
* Audio compression: __AAC__
*/
HEVC_1920x1080 = 9,
/**
* Resolution: __3840 × 2160__ •
* Video compression: __HEVC__ •
* Audio compression: __AAC__
*/
HEVC_3840x2160 = 10,
}
// @needsAudit
export enum UIImagePickerControllerQualityType {
/**
* Highest available resolution.
*/
High = 0,
/**
* Depends on the device.
*/
Medium = 1,
/**
* Depends on the device.
*/
Low = 2,
/**
* 640 × 480
*/
VGA640x480 = 3,
/**
* 1280 × 720
*/
IFrame1280x720 = 4,
/**
* 960 × 540
*/
IFrame960x540 = 5,
}
/**
* Picker presentation style. Its values are directly mapped to the [`UIModalPresentationStyle`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621355-modalpresentationstyle).
*
* @platform ios
*/
export enum UIImagePickerPresentationStyle {
/**
* A presentation style in which the presented picker covers the screen.
*/
FULL_SCREEN = 'fullScreen',
/**
* A presentation style that partially covers the underlying content.
*/
PAGE_SHEET = 'pageSheet',
/**
* A presentation style that displays the picker centered in the screen.
*/
FORM_SHEET = 'formSheet',
/**
* A presentation style where the picker is displayed over the app's content.
*/
CURRENT_CONTEXT = 'currentContext',
/**
* A presentation style in which the picker view covers the screen.
*/
OVER_FULL_SCREEN = 'overFullScreen',
/**
* A presentation style where the picker is displayed over the app's content.
*/
OVER_CURRENT_CONTEXT = 'overCurrentContext',
/**
* A presentation style where the picker is displayed in a popover view.
*/
POPOVER = 'popover',
/**
* The default presentation style chosen by the system.
* On older iOS versions, falls back to `WebBrowserPresentationStyle.FullScreen`.
*
* @platform ios
*/
AUTOMATIC = 'automatic',
}
/**
* Picker preferred asset representation mode. Its values are directly mapped to the [`PHPickerConfigurationAssetRepresentationMode`](https://developer.apple.com/documentation/photokit/phpickerconfigurationassetrepresentationmode).
*
* @platform ios
*/
export enum UIImagePickerPreferredAssetRepresentationMode {
/**
* A mode that indicates that the system chooses the appropriate asset representation.
*/
Automatic = 'automatic',
/**
* A mode that uses the most compatible asset representation.
*/
Compatible = 'compatible',
/**
* A mode that uses the current representation to avoid transcoding, if possible.
*/
Current = 'current',
}
export enum CameraType {
/**
* Back/rear camera.
*/
back = 'back',
/**
* Front camera
*/
front = 'front',
}
/**
* @hidden
* @deprecated Use `ImagePickerAsset` instead
*/
export type ImageInfo = ImagePickerAsset;
/**
* Represents an asset (image or video) returned by the image picker or camera.
*/
export type ImagePickerAsset = {
/**
* URI to the local image or video file (usable as the source of an `Image` element, in the case of
* an image) and `width` and `height` specify the dimensions of the media.
*/
uri: string;
/**
* The unique ID that represents the picked image or video, if picked from the library. It can be used
* by [expo-media-library](./media-library) to manage the picked asset.
*
* > This might be `null` when the ID is unavailable or the user gave limited permission to access the media library.
* > On Android, the ID is unavailable when the user selects a photo by directly browsing file system.
*
* @platform ios
* @platform android
*/
assetId?: string | null;
/**
* Width of the image or video.
*/
width: number;
/**
* Height of the image or video.
*/
height: number;
/**
* The type of the asset.
*/
type?: 'image' | 'video';
/**
* Preferred filename to use when saving this item. This might be `null` when the name is unavailable
* or user gave limited permission to access the media library.
*
*/
fileName?: string | null;
/**
* File size of the picked image or video, in bytes.
*
*/
fileSize?: number;
/**
* The `exif` field is included if the `exif` option is truthy, and is an object containing the
* image's EXIF data. The names of this object's properties are EXIF tags and the values are the
* respective EXIF values for those tags.
*/
exif?: Record<string, any> | null;
/**
* When the `base64` option is truthy, it is a Base64-encoded string of the selected image's JPEG data, otherwise `null`.
* If you prepend this with `'data:image/jpeg;base64,'` to create a data URI,
* you can use it as the source of an `Image` element; for example:
* ```ts
* <Image
* source={{ uri: 'data:image/jpeg;base64,' + asset.base64 }}
* style={{ width: 200, height: 200 }}
* />
* ```
*/
base64?: string | null;
/**
* Length of the video in milliseconds or `null` if the asset is not a video.
*/
duration?: number | null;
/**
* The MIME type of the selected asset or `null` if could not be determined.
*/
mimeType?: string;
};
// @needsAudit
export type ImagePickerErrorResult = {
/**
* The error code.
*/
code: string;
/**
* The error message.
*/
message: string;
/**
* The exception which caused the error.
*/
exception?: string;
};
// @needsAudit
/**
* Type representing successful and canceled pick result.
*/
export type ImagePickerResult = ImagePickerSuccessResult | ImagePickerCanceledResult;
/**
* Type representing successful pick result.
*/
export type ImagePickerSuccessResult = {
/**
* Boolean flag set to `false` showing that the request was successful.
*/
canceled: false;
/**
* An array of picked assets.
*/
assets: ImagePickerAsset[];
};
/**
* Type representing canceled pick result.
*/
export type ImagePickerCanceledResult = {
/**
* Boolean flag set to `true` showing that the request was canceled.
*/
canceled: true;
/**
* `null` signifying that the request was canceled.
*/
assets: null;
};
/**
* @hidden
* @deprecated Use `ImagePickerResult` instead.
*/
export type ImagePickerCancelledResult = ImagePickerCanceledResult;
/**
* @hidden
* @deprecated `ImagePickerMultipleResult` has been deprecated in favor of `ImagePickerResult`.
*/
export type ImagePickerMultipleResult = ImagePickerResult;
// @needsAudit
export type ImagePickerOptions = {
/**
* Whether to show a UI to edit the image after it is picked. On Android the user can crop and
* rotate the image and on iOS simply crop it.
*
* > - Cropping multiple images is not supported - this option is mutually exclusive with `allowsMultipleSelection`.
* > - On iOS, this option is ignored if `allowsMultipleSelection` is enabled.
* > - On iOS cropping a `.bmp` image will convert it to `.png`.
*
* @default false
* @platform ios
* @platform android
*/
allowsEditing?: boolean;
/**
* An array with two entries `[x, y]` specifying the aspect ratio to maintain if the user is
* allowed to edit the image (by passing `allowsEditing: true`). This is only applicable on
* Android, since on iOS the crop rectangle is always a square.
*/
aspect?: [number, number];
/**
* Specify the quality of compression, from `0` to `1`. `0` means compress for small size,
* `1` means compress for maximum quality.
* > Note: If the selected image has been compressed before, the size of the output file may be
* > bigger than the size of the original image.
*
* > Note: On iOS, if a `.bmp` or `.png` image is selected from the library, this option is ignored.
*
* @default 0.2
* @platform ios
* @platform android
*/
quality?: number;
/**
* Choose what type of media to pick.
* @default ImagePicker.MediaTypeOptions.Images
*/
mediaTypes?: MediaTypeOptions;
/**
* Whether to also include the EXIF data for the image. On iOS the EXIF data does not include GPS
* tags in the camera case.
*/
exif?: boolean;
/**
* Whether to also include the image data in Base64 format.
*/
base64?: boolean;
/**
* Specify preset which will be used to compress selected video.
* @default ImagePicker.VideoExportPreset.Passthrough
* @platform ios 11+
* @deprecated See [`videoExportPreset`](https://developer.apple.com/documentation/uikit/uiimagepickercontroller/2890964-videoexportpreset?language=objc)
* in Apple documentation.
*/
videoExportPreset?: VideoExportPreset;
/**
* Specify the quality of recorded videos. Defaults to the highest quality available for the device.
* @default ImagePicker.UIImagePickerControllerQualityType.High
* @platform ios
*/
videoQuality?: UIImagePickerControllerQualityType;
/**
* Whether or not to allow selecting multiple media files at once.
*
* > Cropping multiple images is not supported - this option is mutually exclusive with `allowsEditing`.
* > If this option is enabled, then `allowsEditing` is ignored.
*
* @default false
* @platform ios 14+
* @platform android
* @platform web
*/
allowsMultipleSelection?: boolean;
/**
* The maximum number of items that user can select. Applicable when `allowsMultipleSelection` is enabled.
* Setting the value to `0` sets the selection limit to the maximum that the system supports.
*
* @platform ios 14+
* @platform android
* @default 0
*/
selectionLimit?: number;
/**
* Whether to display number badges when assets are selected. The badges are numbered
* in selection order. Assets are then returned in the exact same order they were selected.
*
* > Assets should be returned in the selection order regardless of this option,
* > but there is no guarantee that it is always true when this option is disabled.
*
* @platform ios 15+
* @default false
*/
orderedSelection?: boolean;
/**
* Maximum duration, in seconds, for video recording. Setting this to `0` disables the limit.
* Defaults to `0` (no limit).
* - **On iOS**, when `allowsEditing` is set to `true`, maximum duration is limited to 10 minutes.
* This limit is applied automatically, if `0` or no value is specified.
* - **On Android**, effect of this option depends on support of installed camera app.
* - **On Web** this option has no effect - the limit is browser-dependant.
*/
videoMaxDuration?: number;
/**
* Choose [presentation style](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621355-modalpresentationstyle?language=objc)
* to customize view during taking photo/video.
* @default ImagePicker.UIImagePickerPresentationStyle.Automatic
* @platform ios
*/
presentationStyle?: UIImagePickerPresentationStyle;
/**
* Selects the camera-facing type. The `CameraType` enum provides two options:
* `front` for the front-facing camera and `back` for the back-facing camera.
* - **On Android**, the behavior of this option may vary based on the camera app installed on the device.
* @default CameraType.back
* @platform ios
* @platform android
*/
cameraType?: CameraType;
/**
* Choose [preferred asset representation mode](https://developer.apple.com/documentation/photokit/phpickerconfigurationassetrepresentationmode)
* to use when loading assets.
* @default ImagePicker.UIImagePickerPreferredAssetRepresentationMode.Automatic
* @platform ios 14+
*/
preferredAssetRepresentationMode?: UIImagePickerPreferredAssetRepresentationMode;
/**
* Uses the legacy image picker on Android. This will allow media to be selected from outside the users photo library.
* @platform android
* @default false
*/
legacy?: boolean;
};
// @needsAudit
export type OpenFileBrowserOptions = {
/**
* Choose what type of media to pick.
* @default ImagePicker.MediaTypeOptions.Images
*/
mediaTypes: MediaTypeOptions;
// @docsMissing
capture?: boolean;
/**
* Whether or not to allow selecting multiple media files at once.
* @platform web
*/
allowsMultipleSelection: boolean;
/**
* Whether to also include the image data in Base64 format.
*/
base64: boolean;
};
/**
* @hidden
* @deprecated Use `ImagePickerResult` or `OpenFileBrowserOptions` instead.
*/
export type ExpandImagePickerResult<T extends ImagePickerOptions | OpenFileBrowserOptions> =
T extends {
allowsMultipleSelection: true;
}
? ImagePickerResult
: ImagePickerResult;

View File

@@ -0,0 +1,9 @@
// @generated by expo-module-scripts
{
"extends": "expo-module-scripts/tsconfig.base",
"compilerOptions": {
"outDir": "./build"
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}