Repository: baronha/react-native-multiple-image-picker
Branch: main
Commit: defc08c12fe7
Files: 239
Total size: 528.8 KB
Directory structure:
gitextract_k5e7rspq/
├── .circleci/
│ └── config.yml
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── docs.yml
├── .gitignore
├── .npmignore
├── CONTRIBUTING.md
├── LICENSE
├── MultipleImagePicker.podspec
├── README.md
├── android/
│ ├── CMakeLists.txt
│ ├── build.gradle
│ ├── gradle.properties
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── cpp/
│ │ └── cpp-adapter.cpp
│ ├── java/
│ │ └── com/
│ │ └── margelo/
│ │ └── nitro/
│ │ └── multipleimagepicker/
│ │ ├── CameraEngine.kt
│ │ ├── Constant.kt
│ │ ├── CropEngine.kt
│ │ ├── ExoPlayerEngine.kt
│ │ ├── GlideEngine.kt
│ │ ├── ImageLoaderUtils.kt
│ │ ├── LoadingDialog.kt
│ │ ├── MultipleImagePicker.kt
│ │ ├── MultipleImagePickerImp.kt
│ │ ├── MultipleImagePickerPackage.java
│ │ ├── PictureSelectorEngineImp.kt
│ │ └── VideoThumbnailEngine.kt
│ └── res/
│ ├── anim/
│ │ └── anim_modal_in.xml
│ ├── drawable/
│ │ ├── checkbox_selector.xml
│ │ ├── complete_button.xml
│ │ ├── ic_checkmark.xml
│ │ ├── ic_down.xml
│ │ ├── num_oval.xml
│ │ ├── picture_not_selected.xml
│ │ ├── picture_selector.xml
│ │ ├── preview_gallery_item.xml
│ │ └── preview_gallery_white_bg.xml
│ ├── layout/
│ │ └── loading_dialog.xml
│ └── values/
│ ├── colors.xml
│ └── styles.xml
├── babel.config.js
├── docs/
│ ├── .gitignore
│ ├── docs/
│ │ ├── CAMERA.mdx
│ │ ├── CONFIG.mdx
│ │ ├── CROP.mdx
│ │ ├── GETTING_STARTED.mdx
│ │ ├── PREVIEW.mdx
│ │ ├── RESULT.mdx
│ │ ├── SHOWCASE/
│ │ │ ├── index.mdx
│ │ │ ├── showcase.css
│ │ │ └── showcase.json
│ │ ├── USAGE.mdx
│ │ └── index.md
│ ├── docusaurus.config.ts
│ ├── package.json
│ ├── patches/
│ │ └── @gorhom+docusaurus-preset+1.0.2.patch
│ ├── sidebars.ts
│ ├── src/
│ │ └── css/
│ │ └── custom.css
│ ├── static/
│ │ └── .nojekyll
│ └── tsconfig.json
├── example/
│ ├── .gitignore
│ ├── App.tsx
│ ├── Gemfile
│ ├── README.md
│ ├── app.json
│ ├── babel.config.js
│ ├── metro.config.js
│ ├── package.json
│ ├── react-native.config.js
│ ├── src/
│ │ ├── assets/
│ │ │ └── index.ts
│ │ ├── common/
│ │ │ └── const.ts
│ │ ├── components/
│ │ │ ├── BottomSheet.tsx
│ │ │ ├── Button.tsx
│ │ │ ├── CheckBox.tsx
│ │ │ ├── CodeTag.tsx
│ │ │ ├── Container.tsx
│ │ │ ├── CounterView.tsx
│ │ │ ├── Divider.tsx
│ │ │ ├── Input.tsx
│ │ │ ├── Row.tsx
│ │ │ ├── SectionView.tsx
│ │ │ ├── SegmentControl.tsx
│ │ │ ├── Text.tsx
│ │ │ ├── View.tsx
│ │ │ └── index.tsx
│ │ ├── hook/
│ │ │ ├── context.ts
│ │ │ ├── index.ts
│ │ │ └── useTheme.ts
│ │ ├── index.tsx
│ │ └── theme/
│ │ ├── color.ts
│ │ └── size.ts
│ └── tsconfig.json
├── ios/
│ ├── Assets.swift
│ ├── Assets.xcassets/
│ │ ├── Contents.json
│ │ └── close.imageset/
│ │ └── Contents.json
│ ├── ErrorCode.swift
│ ├── HybridMultipleImagePicker+Camera.swift
│ ├── HybridMultipleImagePicker+Config.swift
│ ├── HybridMultipleImagePicker+Crop.swift
│ ├── HybridMultipleImagePicker+Preview.swift
│ ├── HybridMultipleImagePicker+Result.swift
│ ├── HybridMultipleImagePicker.swift
│ ├── MultipleImagePickerOnLoad.mm
│ ├── PHAsset+Thumbnail.swift
│ ├── PhotoCancelItem.swift
│ ├── TopViewController.swift
│ ├── UIColor+Hex.swift
│ ├── UIColor+React.swift
│ ├── UIImage.swift
│ ├── URL+Mime.swift
│ └── Utils.swift
├── nitro.json
├── nitrogen/
│ └── generated/
│ ├── .gitattributes
│ ├── android/
│ │ ├── MultipleImagePicker+autolinking.cmake
│ │ ├── MultipleImagePicker+autolinking.gradle
│ │ ├── MultipleImagePickerOnLoad.cpp
│ │ ├── MultipleImagePickerOnLoad.hpp
│ │ ├── c++/
│ │ │ ├── JCameraDevice.hpp
│ │ │ ├── JCameraResult.hpp
│ │ │ ├── JCropRatio.hpp
│ │ │ ├── JCropResult.hpp
│ │ │ ├── JFunc_void_CameraResult.hpp
│ │ │ ├── JFunc_void_CropResult.hpp
│ │ │ ├── JFunc_void_double.hpp
│ │ │ ├── JFunc_void_std__vector_PickerResult_.hpp
│ │ │ ├── JHybridMultipleImagePickerSpec.cpp
│ │ │ ├── JHybridMultipleImagePickerSpec.hpp
│ │ │ ├── JLanguage.hpp
│ │ │ ├── JMediaPreview.hpp
│ │ │ ├── JMediaType.hpp
│ │ │ ├── JNitroCameraConfig.hpp
│ │ │ ├── JNitroConfig.hpp
│ │ │ ├── JNitroCropConfig.hpp
│ │ │ ├── JNitroPreviewConfig.hpp
│ │ │ ├── JPickerCameraConfig.hpp
│ │ │ ├── JPickerCropConfig.hpp
│ │ │ ├── JPickerResult.hpp
│ │ │ ├── JPresentation.hpp
│ │ │ ├── JResultType.hpp
│ │ │ ├── JSelectBoxStyle.hpp
│ │ │ ├── JSelectMode.hpp
│ │ │ ├── JText.hpp
│ │ │ └── JTheme.hpp
│ │ └── kotlin/
│ │ └── com/
│ │ └── margelo/
│ │ └── nitro/
│ │ └── multipleimagepicker/
│ │ ├── CameraDevice.kt
│ │ ├── CameraResult.kt
│ │ ├── CropRatio.kt
│ │ ├── CropResult.kt
│ │ ├── Func_void_CameraResult.kt
│ │ ├── Func_void_CropResult.kt
│ │ ├── Func_void_double.kt
│ │ ├── Func_void_std__vector_PickerResult_.kt
│ │ ├── HybridMultipleImagePickerSpec.kt
│ │ ├── Language.kt
│ │ ├── MediaPreview.kt
│ │ ├── MediaType.kt
│ │ ├── MultipleImagePickerOnLoad.kt
│ │ ├── NitroCameraConfig.kt
│ │ ├── NitroConfig.kt
│ │ ├── NitroCropConfig.kt
│ │ ├── NitroPreviewConfig.kt
│ │ ├── PickerCameraConfig.kt
│ │ ├── PickerCropConfig.kt
│ │ ├── PickerResult.kt
│ │ ├── Presentation.kt
│ │ ├── ResultType.kt
│ │ ├── SelectBoxStyle.kt
│ │ ├── SelectMode.kt
│ │ ├── Text.kt
│ │ └── Theme.kt
│ ├── ios/
│ │ ├── MultipleImagePicker+autolinking.rb
│ │ ├── MultipleImagePicker-Swift-Cxx-Bridge.cpp
│ │ ├── MultipleImagePicker-Swift-Cxx-Bridge.hpp
│ │ ├── MultipleImagePicker-Swift-Cxx-Umbrella.hpp
│ │ ├── MultipleImagePickerAutolinking.mm
│ │ ├── MultipleImagePickerAutolinking.swift
│ │ ├── c++/
│ │ │ ├── HybridMultipleImagePickerSpecSwift.cpp
│ │ │ └── HybridMultipleImagePickerSpecSwift.hpp
│ │ └── swift/
│ │ ├── CameraDevice.swift
│ │ ├── CameraResult.swift
│ │ ├── CropRatio.swift
│ │ ├── CropResult.swift
│ │ ├── Func_void_CameraResult.swift
│ │ ├── Func_void_CropResult.swift
│ │ ├── Func_void_double.swift
│ │ ├── Func_void_std__vector_PickerResult_.swift
│ │ ├── HybridMultipleImagePickerSpec.swift
│ │ ├── HybridMultipleImagePickerSpec_cxx.swift
│ │ ├── Language.swift
│ │ ├── MediaPreview.swift
│ │ ├── MediaType.swift
│ │ ├── NitroCameraConfig.swift
│ │ ├── NitroConfig.swift
│ │ ├── NitroCropConfig.swift
│ │ ├── NitroPreviewConfig.swift
│ │ ├── PickerCameraConfig.swift
│ │ ├── PickerCropConfig.swift
│ │ ├── PickerResult.swift
│ │ ├── Presentation.swift
│ │ ├── ResultType.swift
│ │ ├── SelectBoxStyle.swift
│ │ ├── SelectMode.swift
│ │ ├── Text.swift
│ │ └── Theme.swift
│ └── shared/
│ └── c++/
│ ├── CameraDevice.hpp
│ ├── CameraResult.hpp
│ ├── CropRatio.hpp
│ ├── CropResult.hpp
│ ├── HybridMultipleImagePickerSpec.cpp
│ ├── HybridMultipleImagePickerSpec.hpp
│ ├── Language.hpp
│ ├── MediaPreview.hpp
│ ├── MediaType.hpp
│ ├── NitroCameraConfig.hpp
│ ├── NitroConfig.hpp
│ ├── NitroCropConfig.hpp
│ ├── NitroPreviewConfig.hpp
│ ├── PickerCameraConfig.hpp
│ ├── PickerCropConfig.hpp
│ ├── PickerResult.hpp
│ ├── Presentation.hpp
│ ├── ResultType.hpp
│ ├── SelectBoxStyle.hpp
│ ├── SelectMode.hpp
│ ├── Text.hpp
│ └── Theme.hpp
├── package.json
├── react-native.config.js
├── scripts/
│ └── bootstrap.js
├── src/
│ ├── index.ts
│ ├── specs/
│ │ └── MultipleImagePicker.nitro.ts
│ └── types/
│ ├── camera.ts
│ ├── config.ts
│ ├── crop.ts
│ ├── error.ts
│ ├── index.ts
│ ├── picker.ts
│ ├── preview.ts
│ └── result.ts
├── tsconfig.build.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
version: 2.1
executors:
default:
docker:
- image: circleci/node:10
working_directory: ~/project
commands:
attach_project:
steps:
- attach_workspace:
at: ~/project
jobs:
install-dependencies:
executor: default
steps:
- checkout
- attach_project
- restore_cache:
keys:
- dependencies-{{ checksum "package.json" }}
- dependencies-
- restore_cache:
keys:
- dependencies-example-{{ checksum "example/package.json" }}
- dependencies-example-
- run:
name: Install dependencies
command: |
yarn install --cwd example --frozen-lockfile
yarn install --frozen-lockfile
- save_cache:
key: dependencies-{{ checksum "package.json" }}
paths: node_modules
- save_cache:
key: dependencies-example-{{ checksum "example/package.json" }}
paths: example/node_modules
- persist_to_workspace:
root: .
paths: .
lint:
executor: default
steps:
- attach_project
- run:
name: Lint files
command: |
yarn lint
typescript:
executor: default
steps:
- attach_project
- run:
name: Typecheck files
command: |
yarn typescript
unit-tests:
executor: default
steps:
- attach_project
- run:
name: Run unit tests
command: |
yarn test --coverage
- store_artifacts:
path: coverage
destination: coverage
build-package:
executor: default
steps:
- attach_project
- run:
name: Build package
command: |
yarn prepare
workflows:
build-and-test:
jobs:
- install-dependencies
- lint:
requires:
- install-dependencies
- typescript:
requires:
- install-dependencies
- unit-tests:
requires:
- install-dependencies
- build-package:
requires:
- install-dependencies
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: baronha
patreon: baronha
open_collective: # Replace with a single Open Collective username
ko_fi: baoha
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/workflows/docs.yml
================================================
name: Deploy to GitHub Pages
on:
push:
branches:
- main
paths:
- 'docs/**'
jobs:
build:
name: Build Docusaurus
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 18
cache: yarn
- name: Install dependencies
working-directory: docs
run: yarn install --frozen-lockfile
- name: Build docs
working-directory: docs
run: yarn build
- name: Upload Build Artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/build
deploy:
name: Deploy to GitHub Pages
needs: build
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
# Deploy to the github-pages environment
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .gitignore
================================================
# OSX
#
.DS_Store
# XDE
.expo/
# VSCode
.vscode/
jsconfig.json
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# Android/IJ
#
.classpath
.cxx
.gradle
.idea
.project
.settings
local.properties
android.iml
# Cocoapods
#
example/ios/Pods
# Ruby
example/vendor/
# node.js
#
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
# BUCK
buck-out/
\.buckd/
android/app/libs
android/keystores/debug.keystore
# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Expo
.expo/
# Turborepo
.turbo/
# generated by bob
lib/
================================================
FILE: .npmignore
================================================
example/
files/
node_modules/
npm-debug.log
package-lock.json
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project.
## Development workflow
To get started with the project, run `yarn` in the root directory to install the required dependencies for each package:
```sh
yarn
```
While developing, you can run the [example app](/example/) to test your changes.
To start the packager:
```sh
yarn example start
```
To run the example app on Android:
```sh
yarn example android
```
To run the example app on iOS:
```sh
yarn example ios
```
Make sure your code passes TypeScript and ESLint. Run the following to verify:
```sh
yarn typescript
yarn lint
```
To fix formatting errors, run the following:
```sh
yarn lint --fix
```
Remember to add tests for your change if possible. Run the unit tests by:
```sh
yarn test
```
To edit the Objective-C files, open `example/ios/MultipleImagePickerExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > react-native-multiple-image-picker`.
To edit the Kotlin files, open `example/android` in Android studio and find the source files at `reactnativemultipleimagepicker` under `Android`.
### Commit message convention
We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages:
- `fix`: bug fixes, e.g. fix crash due to deprecated method.
- `feat`: new features, e.g. add new method to the module.
- `refactor`: code refactor, e.g. migrate from class components to hooks.
- `docs`: changes into documentation, e.g. add usage example for the module..
- `test`: adding or updating tests, e.g. add integration tests using detox.
- `chore`: tooling changes, e.g. change CI config.
Our pre-commit hooks verify that your commit message matches this format when committing.
### Linting and tests
[ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/)
We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing.
Our pre-commit hooks verify that the linter and tests pass when committing.
### Scripts
The `package.json` file contains various scripts for common tasks:
- `yarn bootstrap`: setup project by installing all dependencies and pods.
- `yarn typescript`: type-check files with TypeScript.
- `yarn lint`: lint files with ESLint.
- `yarn test`: run unit tests with Jest.
- `yarn example start`: start the Metro server for the example app.
- `yarn example android`: run the example app on Android.
- `yarn example ios`: run the example app on iOS.
### Sending a pull request
> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
When you're sending a pull request:
- Prefer small pull requests focused on one change.
- Verify that linters and tests are passing.
- Review the documentation to make sure it looks good.
- Follow the pull request template when opening a pull request.
- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue.
## Code of Conduct
### Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
### Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
### Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
### Scope
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
### Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
#### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
#### 2. Warning
**Community Impact**: A violation through a single incident or series of actions.
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
#### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
#### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Baron Ha.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MultipleImagePicker.podspec
================================================
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "MultipleImagePicker"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = package["author"]
s.platforms = { :ios => min_ios_version_supported }
s.source = { :git => "https://github.com/baronha/react-native-multiple-image-picker.git", :tag => "#{s.version}" }
s.source_files = [
# Implementation (Swift)
"ios/**/*.{swift}",
# Autolinking/Registration (Objective-C++)
"ios/**/*.{m,mm}",
# Implementation (C++ objects)
"cpp/**/*.{hpp,cpp}",
]
s.resource_bundles = {
"MultipleImagePicker" => ["ios/Assets.xcassets"]
}
s.dependency "HXPhotoPicker/Picker", "4.2.4"
s.dependency "HXPhotoPicker/Camera/Lite", "4.2.4"
s.dependency "HXPhotoPicker/Editor", "4.2.4"
s.pod_target_xcconfig = {
# C++ compiler flags, mainly for folly.
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
"GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES FOLLY_MOBILE"
}
if ENV["USE_FRAMEWORKS"]
s.dependency "React-Core"
add_dependency(s, "React-jsinspector", :framework_name => "jsinspector_modern")
add_dependency(s, "React-rendererconsistency", :framework_name => "React_rendererconsistency")
add_dependency(s, "React-jsinspectortracing", :framework_name => 'jsinspector_moderntracing')
add_dependency(s, "React-jsinspectorcdp", :framework_name => 'jsinspector_moderncdp')
end
load 'nitrogen/generated/ios/MultipleImagePicker+autolinking.rb'
add_nitrogen_files(s)
s.dependency 'React-jsi'
s.dependency 'React-callinvoker'
install_modules_dependencies(s)
end
================================================
FILE: README.md
================================================
![Logo][Logo]
[![iOS][iOS]][iOS-URL] [![Android][Android]][Android-URL] [![Swift][Swift]][Swift-URL] [![Kotlin][Kotlin]][Kotlin-URL] [![React-Native][React-Native]][React-Native-URL]
## Overview 🎇
https://github.com/user-attachments/assets/79580bc7-237c-46b7-b92e-1479cc6d9079
React Native Multiple Image Picker **(RNMIP)** enables application to pick images and videos from multiple smart album in iOS/Android. React Native Multiple Image Picker is based on two libraries available, [HXPhotoPicker](https://github.com/SilenceLove/HXPhotoPicker) and [PictureSelector](https://github.com/LuckSiege/PictureSelector)
## Documentation 📚
## Features 🔥
| 🤩 | ![Logo][Logo] |
| --- | ------------------------------------------------------------------------------------- |
| 🍕 | [**Crop**](/docs/docs/CROP.mdx) single/multiple image. |
| 🎑 | [**Preview**](/docs/docs/PREVIEW.mdx) image/video. |
| 📸 | [**Camera**](/docs/docs/CAMERA.mdx) module for capturing photos and recording videos. |
| 🐳 | Keep the previous selection. |
| 0️⃣ | Selected order index. |
| 🎨 | UI Customization (numberOfColumn, spacing, primaryColor ... ) |
| 🌚 | Dark Mode, Light Mode |
| 🌄 | Choose multiple images/video. |
| 📦 | Support smart album `(camera roll, selfies, panoramas, favorites, videos...)`. |
| 📺 | Display video duration. |
| ⛅️ | Support iCloud Photo Library. |
| 🌪 | Scrolling performance. ☕️ |
## Requirements
Because RNMIP uses Nitro Module, it complies with Nitro Modules' requirements.
View Nitro Modules' requirements [here](https://nitro.margelo.com/docs/minimum-requirements)
- `Xcode 16+`
- `iOS 13+`
- `react-native 0.75+`
- `compileSdkVersion 34+`
## Installation
See more [**Installation**](https://nitrogenzlab.github.io/react-native-multiple-image-picker/getting-started)
## Usage
Here is a simple usage of the Multiple Image Picker.
See more [**Config**](https://nitrogenzlab.github.io/react-native-multiple-image-picker/config)
```typescript
import { openPicker, Config } from '@baronha/react-native-multiple-image-picker'
const config: Config = {
maxSelect: 10,
maxVideo: 10,
primaryColor: '#FB9300',
backgroundDark: '#2f2f2f',
numberOfColumn: 4,
mediaType: 'all',
selectBoxStyle: 'number',
selectMode: 'multiple',
language: 'vi', // 🇻🇳 Vietnamese
theme: 'dark',
isHiddenOriginalButton: false,
primaryColor: '#F6B35D',
}
const onPicker = async () => {
try {
const response = await openPicker(config)
setImages(response)
} catch (e) {
// catch error for multiple image picker
}
}
```
## To Do
- [x] Crop Image in iOS.
- [x] Preview Controller for `iOS`.
- [x] Handle Permission when limited on `iOS`.
- [x] Migrating Library to the New Architecture.
- [x] Multiple Crop Image.
- [x] Multiple Preview Image.
- [x] Dynamic Theme.
- [x] Dynamic Language
- [x] Open Crop Controller.
- [x] Open Preview Controller.
- [x] Open Camera Controller.
## Sponsor & Support ☕️
[![BuyMeACoffee][BuyMeACoffee]][BuyMeACoffee-URL] [![Kofi][Kofi]][Kofi-URL]
[BuyMeACoffee]: https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black
[BuyMeACoffee-URL]: https://buymeacoffee.com/baronha
[Kofi]: https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white
[Kofi-URL]: https://ko-fi.com/baoha
To keep this library maintained and up-to-date please consider [sponsoring it on GitHub](https://github.com/sponsors/baronha). Or if you are looking for a private support or help in customizing the experience, then reach out to me on Twitter [@\_baronha](https://twitter.com/_baronha).
Besides, I also built a product using HXPhotoPicker here, Hope you support:
## Built With ❤️
[](https://nitro.margelo.com/docs/nitro-modules)
[](https://github.com/SilenceLove/HXPhotoPicker)
[](https://github.com/LuckSiege/PictureSelector)
## Star History
[](https://star-history.com/#bytebase/star-history&Date)
## Showcase ✨
List of used applications with `@baronha/react-native-multiple-image-picker`
Contributions are welcome! If you have an application that uses `@baronha/react-native-multiple-image-picker`, please open a [pull request](/docs/docs/SHOWCASE/showcase.json) to add it to the list.
See all [**Showcase**](https://nitrogenzlab.github.io/react-native-multiple-image-picker/showcase)
## Performance
We're trying to improve performance. If you have a better solution, please open a [issue](https://github.com/NitrogenZLab/react-native-multiple-image-picker/issues)
or [pull request](https://github.com/NitrogenZLab/react-native-multiple-image-picker/pulls). Best regards!
## License
MIT
[TLPhotoPicker](https://github.com/tilltue/TLPhotoPicker/blob/master/LICENSE)
[PictureSelector](https://github.com/LuckSiege/PictureSelector/blob/master/LICENSE)
[iOS]: https://img.shields.io/badge/iOS-000000?style=for-the-badge&logo=ios&logoColor=white
[iOS-URL]: https://www.apple.com/ios
[Android]: https://img.shields.io/badge/Android-3DDC84?style=for-the-badge&logo=android&logoColor=white
[Android-URL]: https://www.android.com/
[React-Native]: https://img.shields.io/badge/React_Native-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
[React-Native-URL]: https://reactnative.dev/
[React-Native]: https://img.shields.io/badge/React_Native-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
[React-Native-URL]: https://reactnative.dev/
[Swift]: https://img.shields.io/badge/Swift-FA7343?style=for-the-badge&logo=swift&logoColor=white
[Swift-URL]: https://developer.apple.com/swift/
[Kotlin]: https://img.shields.io/badge/Kotlin-0095D5?&style=for-the-badge&logo=kotlin&logoColor=white
[Kotlin-URL]: https://kotlinlang.org/
[Logo]: https://img.shields.io/badge/React_Native_Multiple_Image_Picker-DF78C3?style=for-the-badge
================================================
FILE: android/CMakeLists.txt
================================================
project(MultipleImagePicker)
cmake_minimum_required(VERSION 3.9.0)
set (PACKAGE_NAME MultipleImagePicker)
set (CMAKE_VERBOSE_MAKEFILE ON)
set (CMAKE_CXX_STANDARD 20)
# Define C++ library and add all sources
add_library(${PACKAGE_NAME} SHARED
src/main/cpp/cpp-adapter.cpp
)
# Add Nitrogen specs :)
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/MultipleImagePicker+autolinking.cmake)
# Set up local includes
include_directories(
"src/main/cpp"
"../cpp"
)
find_library(LOG_LIB log)
# Link all libraries together
target_link_libraries(
${PACKAGE_NAME}
${LOG_LIB}
android # <-- Android core
)
================================================
FILE: android/build.gradle
================================================
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.2.1"
}
}
def reactNativeArchitectures() {
def value = rootProject.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}
def isNewArchitectureEnabled() {
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
}
apply plugin: "com.android.library"
apply plugin: 'org.jetbrains.kotlin.android'
apply from: '../nitrogen/generated/android/MultipleImagePicker+autolinking.gradle'
if (isNewArchitectureEnabled()) {
apply plugin: "com.facebook.react"
}
def getExtOrDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["MultipleImagePicker_" + name]
}
def getExtOrIntegerDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["MultipleImagePicker_" + name]).toInteger()
}
android {
namespace "com.margelo.nitro.multipleimagepicker"
ndkVersion getExtOrDefault("ndkVersion")
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
defaultConfig {
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
externalNativeBuild {
cmake {
cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all"
arguments "-DANDROID_STL=c++_shared"
abiFilters(*reactNativeArchitectures())
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
packagingOptions {
excludes = ["META-INF",
"META-INF/**",
"**/libc++_shared.so",
"**/libfbjni.so",
"**/libjsi.so",
"**/libfolly_json.so",
"**/libfolly_runtime.so",
"**/libglog.so",
"**/libhermes.so",
"**/libhermes-executor-debug.so",
"**/libhermes_executor.so",
"**/libreactnative.so",
"**/libreactnativejni.so",
"**/libturbomodulejsijni.so",
"**/libreact_nativemodule_core.so",
"**/libjscexecutor.so"]
}
buildFeatures {
buildConfig true
prefab true
}
buildTypes {
release {
minifyEnabled false
}
}
lintOptions {
disable "GradleCompatible"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
if (isNewArchitectureEnabled()) {
java.srcDirs += [
// React Codegen files
"${project.buildDir}/generated/source/codegen/java"]
}
}
}
}
repositories {
mavenCentral()
google()
}
dependencies {
// For < 0.71, this will be from the local maven repo
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
//noinspection GradleDynamicVersion
// Add a dependency on NitroModules
implementation 'com.github.bumptech.glide:glide:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
// PictureSelector basic (Necessary)
implementation 'io.github.lucksiege:pictureselector:v3.11.2'
// image compress library (Not necessary)
implementation 'io.github.lucksiege:compress:v3.11.2'
// uCrop library (Not necessary)
implementation 'io.github.lucksiege:ucrop:v3.11.2'
// simple camerax library (Not necessary)
implementation 'io.github.lucksiege:camerax:v3.11.2'
// exoplayer
implementation "com.google.android.exoplayer:exoplayer:2.19.1"
implementation "com.facebook.react:react-native:+"
// Add a dependency on NitroModules
implementation project(":react-native-nitro-modules")
}
if (isNewArchitectureEnabled()) {
react {
jsRootDir = file("../src/")
libraryName = "MultipleImagePicker"
codegenJavaPackageName = "com.margelo.nitro.multipleimagepicker"
}
}
================================================
FILE: android/gradle.properties
================================================
MultipleImagePicker_kotlinVersion=1.9.24
MultipleImagePicker_minSdkVersion=23
MultipleImagePicker_targetSdkVersion=34
MultipleImagePicker_compileSdkVersion=34
MultipleImagePicker_ndkVersion=26.1.10909125
================================================
FILE: android/src/main/AndroidManifest.xml
================================================
================================================
FILE: android/src/main/cpp/cpp-adapter.cpp
================================================
#include
#include "MultipleImagePickerOnLoad.hpp"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
return margelo::nitro::multipleimagepicker::initialize(vm);
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/CameraEngine.kt
================================================
package com.margelo.nitro.multipleimagepicker
import android.content.Context
import android.graphics.Color
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.facebook.react.bridge.ColorPropConverter
import com.luck.lib.camerax.SimpleCameraX
import com.luck.picture.lib.interfaces.OnCameraInterceptListener
import java.io.File
class CameraEngine(
private val appContext: Context,
val config: NitroCameraConfig,
) :
OnCameraInterceptListener {
override fun openCamera(fragment: Fragment, cameraMode: Int, requestCode: Int) {
val camera = SimpleCameraX.of()
camera.setImageEngine { context, url, imageView ->
Glide.with(context).load(url).into(imageView)
}
camera.isAutoRotation(true)
camera.setCameraMode(cameraMode)
camera.isDisplayRecordChangeTime(true)
camera.isManualFocusCameraPreview(true)
camera.isZoomCameraPreview(true)
camera.setRecordVideoMaxSecond(config.videoMaximumDuration?.toInt() ?: 60)
camera.setCameraAroundState(config.cameraDevice == CameraDevice.FRONT)
camera.setOutputPathDir(getSandboxCameraOutputPath())
config.color?.let {
val primaryColor = ColorPropConverter.getColor(it, appContext) ?: Color.BLACK
camera.setCaptureLoadingColor(primaryColor)
}
camera.start(fragment.requireActivity(), fragment, requestCode)
}
private fun getSandboxCameraOutputPath(): String {
val externalFilesDir: File? = appContext.getExternalFilesDir("")
val customFile = File(externalFilesDir?.absolutePath, "Sandbox")
if (!customFile.exists()) {
customFile.mkdirs()
}
return customFile.absolutePath + File.separator
}
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/Constant.kt
================================================
package com.margelo.nitro.multipleimagepicker
object Constant {
const val TOOLBAR_TEXT_SIZE = 12
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/CropEngine.kt
================================================
package com.margelo.nitro.multipleimagepicker
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.widget.ImageView
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.luck.picture.lib.config.PictureMimeType
import com.luck.picture.lib.engine.CropFileEngine
import com.luck.picture.lib.entity.LocalMedia
import com.luck.picture.lib.interfaces.OnMediaEditInterceptListener
import com.luck.picture.lib.utils.DateUtils
import com.margelo.nitro.multipleimagepicker.ImageLoaderUtils.assertValidRequest
import com.yalantis.ucrop.UCrop
import com.yalantis.ucrop.UCrop.Options
import com.yalantis.ucrop.UCropImageEngine
import java.io.File
class CropImageEngine : UCropImageEngine {
override fun loadImage(context: Context, url: String, imageView: ImageView) {
if (!assertValidRequest(context)) {
return
}
Glide.with(context).load(url).override(180, 180).into(imageView)
}
override fun loadImage(
context: Context,
url: Uri,
maxWidth: Int,
maxHeight: Int,
call: UCropImageEngine.OnCallbackListener
) {
Glide.with(context).asBitmap().load(url).override(maxWidth, maxHeight)
.into(object : CustomTarget() {
override fun onResourceReady(
resource: Bitmap, transition: Transition?
) {
call.onCall(resource)
}
override fun onLoadCleared(placeholder: Drawable?) {
call.onCall(null)
}
})
}
}
class CropEngine(cropOption: Options) : CropFileEngine {
private val options: Options = cropOption
override fun onStartCrop(
fragment: Fragment,
srcUri: Uri?,
destinationUri: Uri?,
dataSource: ArrayList?,
requestCode: Int
) {
val uCrop = UCrop.of(srcUri!!, destinationUri!!, dataSource)
uCrop.withOptions(options)
uCrop.setImageEngine(CropImageEngine())
uCrop.start(fragment.requireActivity(), fragment, requestCode)
}
}
class MediaEditInterceptListener(
private val outputCropPath: String,
private val options: Options,
) : OnMediaEditInterceptListener {
override fun onStartMediaEdit(
fragment: Fragment, currentLocalMedia: LocalMedia, requestCode: Int
) {
val currentEditPath = currentLocalMedia.availablePath
val inputUri =
if (PictureMimeType.isContent(currentEditPath)) Uri.parse(currentEditPath)
else Uri.fromFile(File(currentEditPath))
val destinationUri = Uri.fromFile(
File(outputCropPath, DateUtils.getCreateFileName("CROP_") + ".jpeg")
)
val uCrop = UCrop.of(inputUri, destinationUri)
uCrop.withOptions(options)
// set engine
uCrop.setImageEngine(CropImageEngine())
// start edit
uCrop.startEdit(fragment.requireActivity(), fragment, requestCode)
}
}
fun getSandboxPath(context: Context): String {
val externalFilesDir: File? = context.getExternalFilesDir("")
val customFile = File(externalFilesDir?.absolutePath, "Sandbox")
if (!customFile.exists()) {
customFile.mkdirs()
}
return customFile.absolutePath + File.separator
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/ExoPlayerEngine.kt
================================================
package com.margelo.nitro.multipleimagepicker
import android.content.Context
import android.net.Uri
import android.view.View
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.luck.picture.lib.config.PictureMimeType
import com.luck.picture.lib.config.SelectorProviders
import com.luck.picture.lib.engine.VideoPlayerEngine
import com.luck.picture.lib.entity.LocalMedia
import com.luck.picture.lib.interfaces.OnPlayerListener
import java.io.File
import java.util.concurrent.CopyOnWriteArrayList
class ExoPlayerEngine : VideoPlayerEngine {
private val listeners = CopyOnWriteArrayList()
override fun onCreateVideoPlayer(context: Context): View {
val exoPlayer = StyledPlayerView(context)
exoPlayer.useController = true
return exoPlayer
}
override fun onStarPlayer(exoPlayer: StyledPlayerView, media: LocalMedia) {
val player = exoPlayer.player
if (player != null) {
val mediaItem: MediaItem
val path = media.availablePath
mediaItem = if (PictureMimeType.isContent(path)) {
MediaItem.fromUri(Uri.parse(path))
} else if (PictureMimeType.isHasHttp(path)) {
MediaItem.fromUri(path)
} else {
MediaItem.fromUri(Uri.fromFile(File(path)))
}
val config = SelectorProviders.getInstance().selectorConfig
player.repeatMode =
if (config.isLoopAutoPlay) Player.REPEAT_MODE_ALL else Player.REPEAT_MODE_OFF
player.setMediaItem(mediaItem)
player.prepare()
player.play()
}
}
override fun onResume(exoPlayer: StyledPlayerView) {
val player = exoPlayer.player
player?.play()
}
override fun onPause(exoPlayer: StyledPlayerView) {
val player = exoPlayer.player
player?.pause()
}
override fun isPlaying(exoPlayer: StyledPlayerView): Boolean {
val player = exoPlayer.player
return player != null && player.isPlaying
}
override fun addPlayListener(playerListener: OnPlayerListener) {
if (!listeners.contains(playerListener)) {
listeners.add(playerListener)
}
}
override fun removePlayListener(playerListener: OnPlayerListener) {
listeners.remove(playerListener)
}
override fun onPlayerAttachedToWindow(exoPlayer: StyledPlayerView) {
val player: Player = ExoPlayer.Builder(exoPlayer.context).build()
exoPlayer.player = player
player.addListener(mPlayerListener)
}
override fun onPlayerDetachedFromWindow(exoPlayer: StyledPlayerView) {
val player = exoPlayer.player
if (player != null) {
player.removeListener(mPlayerListener)
player.release()
exoPlayer.player = null
}
}
override fun destroy(exoPlayer: StyledPlayerView) {
val player = exoPlayer.player
if (player != null) {
player.removeListener(mPlayerListener)
player.release()
}
}
private val mPlayerListener: Player.Listener = object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
for (i in listeners.indices) {
val playerListener = listeners[i]
playerListener.onPlayerError()
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_READY) {
for (i in listeners.indices) {
val playerListener = listeners[i]
playerListener.onPlayerReady()
}
} else if (playbackState == Player.STATE_BUFFERING) {
for (i in listeners.indices) {
val playerListener = listeners[i]
playerListener.onPlayerLoading()
}
} else if (playbackState == Player.STATE_ENDED) {
for (i in listeners.indices) {
val playerListener = listeners[i]
playerListener.onPlayerEnd()
}
}
}
}
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/GlideEngine.kt
================================================
package com.margelo.nitro.multipleimagepicker
import android.content.Context
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.luck.picture.lib.engine.ImageEngine
import com.luck.picture.lib.utils.ActivityCompatHelper
class GlideEngine private constructor() : ImageEngine {
override fun loadImage(context: Context, url: String, imageView: ImageView) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return
}
Glide.with(context)
.load(url)
.into(imageView)
}
override fun loadImage(
context: Context,
imageView: ImageView,
url: String,
maxWidth: Int,
maxHeight: Int
) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return
}
Glide.with(context)
.load(url)
.override(maxWidth, maxHeight)
.into(imageView)
}
override fun loadAlbumCover(context: Context, url: String, imageView: ImageView) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return
}
Glide.with(context)
.asBitmap()
.load(url)
.override(180, 180)
.sizeMultiplier(0.5f)
.transform(CenterCrop(), RoundedCorners(8))
.into(imageView)
}
override fun loadGridImage(context: Context, url: String, imageView: ImageView) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return
}
Glide.with(context)
.load(url)
.override(200, 200)
.centerCrop()
.placeholder(com.luck.picture.lib.R.drawable.ps_image_placeholder)
.into(imageView)
}
override fun pauseRequests(context: Context) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return
}
Glide.with(context).pauseRequests()
}
override fun resumeRequests(context: Context) {
if (!ActivityCompatHelper.assertValidRequest(context)) {
return
}
Glide.with(context).resumeRequests()
}
private object InstanceHolder {
val instance = GlideEngine()
}
companion object {
fun createGlideEngine(): GlideEngine {
return InstanceHolder.instance
}
}
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/ImageLoaderUtils.kt
================================================
package com.margelo.nitro.multipleimagepicker
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
object ImageLoaderUtils {
fun assertValidRequest(context: Context?): Boolean {
if (context is Activity) {
return !isDestroy(context)
} else if (context is ContextWrapper) {
if (context.baseContext is Activity) {
val activity = context.baseContext as Activity
return !isDestroy(activity)
}
}
return true
}
private fun isDestroy(activity: Activity?): Boolean {
return if (activity == null) {
true
} else activity.isFinishing || activity.isDestroyed
}
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/LoadingDialog.kt
================================================
package com.margelo.nitro.multipleimagepicker
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.Gravity
import android.view.ViewGroup
class LoadingDialog(context: Context?) :
Dialog(context!!, R.style.Picture_Theme_AlertDialog) {
init {
setCancelable(true)
setCanceledOnTouchOutside(false)
}
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.loading_dialog)
setDialogSize()
}
private fun setDialogSize() {
val params = window!!.attributes
params.width = ViewGroup.LayoutParams.WRAP_CONTENT
params.height = ViewGroup.LayoutParams.WRAP_CONTENT
params.gravity = Gravity.CENTER
window!!.setWindowAnimations(R.style.PictureThemeDialogWindowStyle)
window!!.attributes = params
}
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/MultipleImagePicker.kt
================================================
package com.margelo.nitro.multipleimagepicker
import com.margelo.nitro.NitroModules
class MultipleImagePicker : HybridMultipleImagePickerSpec() {
override val memorySize: Long
get() = 5
private val pickerModule = MultipleImagePickerImp(NitroModules.applicationContext)
override fun openPicker(
config: NitroConfig,
resolved: (result: Array) -> Unit,
rejected: (reject: Double) -> Unit
) {
pickerModule.openPicker(config, resolved, rejected)
}
override fun openCrop(
image: String,
config: NitroCropConfig,
resolved: (result: CropResult) -> Unit,
rejected: (reject: Double) -> Unit
) {
pickerModule.openCrop(image, config, resolved, rejected)
}
override fun openPreview(
media: Array,
index: Double,
config: NitroPreviewConfig,
onLongPress: (index: Double) -> Unit
) {
pickerModule.openPreview(media, index.toInt(), config, onLongPress)
}
override fun openCamera(
config: NitroCameraConfig,
resolved: (result: CameraResult) -> Unit,
rejected: (reject: Double) -> Unit
) {
pickerModule.openCamera(config, resolved, rejected)
}
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/MultipleImagePickerImp.kt
================================================
package com.margelo.nitro.multipleimagepicker
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import androidx.core.content.ContextCompat
import com.facebook.react.bridge.BaseActivityEventListener
import com.facebook.react.bridge.ColorPropConverter
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.luck.picture.lib.app.IApp
import com.luck.picture.lib.app.PictureAppMaster
import com.luck.picture.lib.basic.PictureSelector
import com.luck.picture.lib.config.PictureMimeType
import com.luck.picture.lib.config.SelectMimeType
import com.luck.picture.lib.config.SelectModeConfig
import com.luck.picture.lib.engine.PictureSelectorEngine
import com.luck.picture.lib.entity.LocalMedia
import com.luck.picture.lib.interfaces.OnCustomLoadingListener
import com.luck.picture.lib.interfaces.OnExternalPreviewEventListener
import com.luck.picture.lib.interfaces.OnMediaEditInterceptListener
import com.luck.picture.lib.interfaces.OnResultCallbackListener
import com.luck.picture.lib.language.LanguageConfig
import com.luck.picture.lib.style.BottomNavBarStyle
import com.luck.picture.lib.style.PictureSelectorStyle
import com.luck.picture.lib.style.PictureWindowAnimationStyle
import com.luck.picture.lib.style.SelectMainStyle
import com.luck.picture.lib.style.TitleBarStyle
import com.luck.picture.lib.utils.DateUtils
import com.luck.picture.lib.utils.DensityUtil
import com.luck.picture.lib.utils.MediaUtils
import com.yalantis.ucrop.UCrop
import com.yalantis.ucrop.UCrop.Options
import com.yalantis.ucrop.UCrop.REQUEST_CROP
import com.yalantis.ucrop.model.AspectRatio
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
class MultipleImagePickerImp(reactContext: ReactApplicationContext?) :
ReactContextBaseJavaModule(reactContext), IApp {
override fun getName(): String {
return "MultipleImagePicker"
}
companion object {
const val TAG = "MultipleImagePicker"
}
private var style = PictureSelectorStyle()
private lateinit var config: NitroConfig
private var cropOption = Options()
private var dataList = mutableListOf()
@ReactMethod
fun openPicker(
options: NitroConfig,
resolved: (result: Array) -> Unit,
rejected: (reject: Double) -> Unit
) {
PictureAppMaster.getInstance().app = this
val activity = reactApplicationContext.currentActivity
?: throw IllegalStateException("No current Activity available")
val imageEngine = GlideEngine.createGlideEngine()
// set global config
config = options
setStyle() // set style for UI
handleSelectedAssets(config)
val chooseMode = getChooseMode(config.mediaType)
val maxSelect = config.maxSelect?.toInt() ?: 20
val maxVideo = config.maxVideo?.toInt() ?: 20
val isPreview = config.isPreview ?: true
val maxFileSize = config.maxFileSize?.toLong()
val maxDuration = config.maxVideoDuration?.toInt()
val minDuration = config.minVideoDuration?.toInt()
val allowSwipeToSelect = config.allowSwipeToSelect ?: false
val isMultiple = config.selectMode == SelectMode.MULTIPLE
val selectMode = if (isMultiple) SelectModeConfig.MULTIPLE else SelectModeConfig.SINGLE
val isCrop = config.crop != null
PictureSelector.create(activity)
.openGallery(chooseMode)
.setImageEngine(imageEngine)
.setSelectedData(dataList)
.setSelectorUIStyle(style)
.apply {
if (isCrop) {
setCropOption(config.crop)
// Disabled force crop engine for multiple
if (!isMultiple) setCropEngine(CropEngine(cropOption))
else setEditMediaInterceptListener(setEditMediaEvent())
}
maxDuration?.let {
setFilterVideoMaxSecond(it)
}
minDuration?.let {
setFilterVideoMinSecond(it)
}
maxFileSize?.let {
setFilterMaxFileSize(it)
}
isDisplayCamera(config.camera != null)
config.camera?.let {
val cameraConfig = NitroCameraConfig(
mediaType = MediaType.ALL,
presentation = Presentation.FULLSCREENMODAL,
language = Language.SYSTEM,
crop = null,
isSaveSystemAlbum = false,
color = config.primaryColor,
cameraDevice = it.cameraDevice,
videoMaximumDuration = it.videoMaximumDuration
)
setCameraInterceptListener(CameraEngine(appContext, cameraConfig))
}
}
.setVideoThumbnailListener(VideoThumbnailEngine(getVideoThumbnailDir()))
.setImageSpanCount(config.numberOfColumn?.toInt() ?: 3)
.setMaxSelectNum(maxSelect)
.isDirectReturnSingle(true)
.isSelectZoomAnim(true)
.isPageStrategy(true, 50)
.isWithSelectVideoImage(true)
.setMaxVideoSelectNum(if (maxVideo != 20) maxVideo else maxSelect)
.isMaxSelectEnabledMask(true)
.isAutoVideoPlay(true)
.isFastSlidingSelect(allowSwipeToSelect)
.isPageSyncAlbumCount(true)
// isPreview
.isPreviewImage(isPreview)
.isPreviewVideo(isPreview)
.isDisplayTimeAxis(true)
.setSelectionMode(selectMode)
.isOriginalControl(config.isHiddenOriginalButton == false)
.setLanguage(getLanguage(config.language))
.isPreviewFullScreenMode(true)
.forResult(object : OnResultCallbackListener {
override fun onResult(localMedia: ArrayList?) {
var data: Array = arrayOf()
if (localMedia?.size == 0 || localMedia == null) {
resolved(arrayOf())
return
}
// set dataList
dataList = localMedia.filterNotNull().toMutableList()
localMedia.forEach { item ->
if (item != null) {
val media = getResult(item)
data += media // Add the media to the data array
}
}
resolved(data)
}
override fun onCancel() {
//
}
})
}
@ReactMethod
fun openCrop(
image: String,
options: NitroCropConfig,
resolved: (result: CropResult) -> Unit,
rejected: (reject: Double) -> Unit
) {
cropOption = Options()
setCropOption(
PickerCropConfig(
circle = options.circle,
ratio = options.ratio,
defaultRatio = options.defaultRatio,
freeStyle = options.freeStyle
)
)
try {
val uri = when {
// image network
image.startsWith("http://") || image.startsWith("https://") -> {
// Handle remote URL
val url = URL(image)
val connection = url.openConnection() as HttpURLConnection
connection.doInput = true
connection.connect()
val inputStream = connection.inputStream
// Create a temp file to store the image
val file = File(appContext.cacheDir, "CROP_")
file.outputStream().use { output ->
inputStream.copyTo(output)
}
Uri.fromFile(file)
}
else -> Uri.parse(image)
}
val destinationUri = Uri.fromFile(
File(getSandboxPath(appContext), DateUtils.getCreateFileName("CROP_") + ".jpeg")
)
val uCrop = UCrop.of(uri, destinationUri).withOptions(cropOption)
// set engine
uCrop.setImageEngine(CropImageEngine())
// start edit
val cropActivityEventListener = object : BaseActivityEventListener() {
override fun onActivityResult(
activity: Activity,
requestCode: Int,
resultCode: Int,
data: Intent?
) {
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CROP) {
val resultUri = UCrop.getOutput(data!!)
val width = UCrop.getOutputImageWidth(data).toDouble()
val height = UCrop.getOutputImageHeight(data).toDouble()
resultUri?.let {
val result = CropResult(
path = it.toString(),
width,
height,
)
resolved(result)
}
} else if (resultCode == UCrop.RESULT_ERROR) {
val cropError = UCrop.getError(data!!)
rejected(0.0)
}
// Remove listener after getting result
reactApplicationContext.removeActivityEventListener(this)
}
}
// Add listener before starting UCrop
reactApplicationContext.addActivityEventListener(cropActivityEventListener)
reactApplicationContext.currentActivity?.let { uCrop.start(it, REQUEST_CROP) }
} catch (e: Exception) {
rejected(0.0)
}
}
@ReactMethod
fun openPreview(
media: Array,
index: Int,
config: NitroPreviewConfig,
onLongPress: (index: Double) -> Unit
) {
val imageEngine = GlideEngine.createGlideEngine()
val assets: ArrayList = arrayListOf()
val previewStyle = PictureSelectorStyle()
val titleBarStyle = TitleBarStyle()
previewStyle.windowAnimationStyle.setActivityEnterAnimation(R.anim.anim_modal_in)
previewStyle.windowAnimationStyle.setActivityExitAnimation(com.luck.picture.lib.R.anim.ps_anim_modal_out)
previewStyle.selectMainStyle.previewBackgroundColor = Color.BLACK
titleBarStyle.previewTitleBackgroundColor = Color.BLACK
previewStyle.titleBarStyle = titleBarStyle
media.withIndex().forEach { (index, mediaItem) ->
var asset: LocalMedia? = null
mediaItem.path?.let { path ->
// network asset
if (path.startsWith("https://") || path.startsWith("http://")) {
val localMedia = LocalMedia.create()
localMedia.path = path
localMedia.mimeType =
if (mediaItem.type == ResultType.VIDEO) "video/mp4" else MediaUtils.getMimeTypeFromMediaHttpUrl(
path
) ?: "image/jpg"
asset = localMedia
} else {
asset = LocalMedia.generateLocalMedia(appContext, path)
}
}
asset?.let {
it.setPosition(index)
assets.add(it)
}
}
val activity = reactApplicationContext.currentActivity
?: throw IllegalStateException("No current Activity available")
PictureSelector
.create(activity)
.openPreview()
.setImageEngine(imageEngine)
.setLanguage(getLanguage(config.language))
.setSelectorUIStyle(previewStyle)
.isPreviewFullScreenMode(true)
.isAutoVideoPlay(config.videoAutoPlay == true)
.setVideoPlayerEngine(ExoPlayerEngine())
.isVideoPauseResumePlay(true)
.setCustomLoadingListener(getCustomLoadingListener())
.setExternalPreviewEventListener(object : OnExternalPreviewEventListener {
override fun onPreviewDelete(position: Int) {
//
}
override fun onLongPressDownload(context: Context, media: LocalMedia): Boolean {
onLongPress(media.position.toDouble())
return true
}
})
.startFragmentPreview(index, false, assets)
}
private fun getCustomLoadingListener(): OnCustomLoadingListener {
return OnCustomLoadingListener { context -> LoadingDialog(context) }
}
@ReactMethod
fun openCamera(
config: NitroCameraConfig,
resolved: (result: CameraResult) -> Unit,
rejected: (reject: Double) -> Unit
) {
val activity = reactApplicationContext.currentActivity
?: throw IllegalStateException("No current Activity available")
val chooseMode = getChooseMode(config.mediaType)
PictureSelector
.create(activity)
.openCamera(chooseMode)
.setLanguage(getLanguage(config.language))
.setCameraInterceptListener(CameraEngine(appContext, config))
.isQuickCapture(true)
.isOriginalControl(true)
.setVideoThumbnailListener(VideoThumbnailEngine(getVideoThumbnailDir()))
.apply {
if (config.crop != null) {
setCropEngine(CropEngine(cropOption))
}
}
.forResult(object : OnResultCallbackListener {
override fun onResult(results: java.util.ArrayList?) {
results?.first()?.let {
val result = getResult(it)
resolved(
CameraResult(
path = result.path,
type = result.type,
width = result.width,
height = result.height,
duration = result.duration,
thumbnail = result.thumbnail,
fileName = result.fileName
)
)
}
}
override fun onCancel() {
// rejected(0.0)
}
})
}
private fun getChooseMode(mediaType: MediaType): Int {
return when (mediaType) {
MediaType.VIDEO -> SelectMimeType.ofVideo()
MediaType.IMAGE -> SelectMimeType.ofImage()
else -> SelectMimeType.ofAll()
}
}
private fun getVideoThumbnailDir(): String {
val externalFilesDir: File? = appContext.getExternalFilesDir("")
val customFile = File(externalFilesDir?.absolutePath, "Thumbnail")
if (!customFile.exists()) {
customFile.mkdirs()
}
return customFile.absolutePath + File.separator
}
private fun getLanguage(language: Language): Int {
return when (language) {
Language.VI -> LanguageConfig.VIETNAM // -> 🇻🇳 My country. Yeahhh
Language.EN -> LanguageConfig.ENGLISH
Language.ZH_HANS -> LanguageConfig.CHINESE
Language.ZH_HANT -> LanguageConfig.TRADITIONAL_CHINESE
Language.DE -> LanguageConfig.GERMANY
Language.KO -> LanguageConfig.KOREA
Language.FR -> LanguageConfig.FRANCE
Language.JA -> LanguageConfig.JAPAN
Language.AR -> LanguageConfig.AR
Language.RU -> LanguageConfig.RU
else -> LanguageConfig.SYSTEM_LANGUAGE
}
}
private fun setCropOption(config: PickerCropConfig?) {
cropOption.setShowCropFrame(true)
cropOption.setShowCropGrid(true)
cropOption.setCircleDimmedLayer(config?.circle ?: false)
cropOption.setCropOutputPathDir(getSandboxPath(appContext))
cropOption.isCropDragSmoothToCenter(true)
cropOption.isForbidSkipMultipleCrop(true)
cropOption.setMaxScaleMultiplier(100f)
cropOption.setToolbarWidgetColor(Color.BLACK)
cropOption.setStatusBarColor(Color.WHITE)
cropOption.isDarkStatusBarBlack(true)
cropOption.isDragCropImages(true)
cropOption.setFreeStyleCropEnabled(config?.freeStyle ?: true)
cropOption.setSkipCropMimeType(*getNotSupportCrop())
val ratioCount = config?.ratio?.size ?: 0
if (config?.defaultRatio != null || ratioCount > 0) {
var ratioList = arrayOf(AspectRatio("Original", 0f, 0f))
if (ratioCount > 0) {
config?.ratio?.take(4)?.toTypedArray()?.forEach { item ->
ratioList += AspectRatio(
item.title, item.width.toFloat(), item.height.toFloat()
)
}
}
// Add default Aspects
ratioList += arrayOf(
AspectRatio(null, 1f, 1f),
AspectRatio(null, 16f, 9f),
AspectRatio(null, 4f, 3f),
AspectRatio(null, 3f, 2f)
)
config?.defaultRatio?.let {
val defaultRatio = AspectRatio(it.title, it.width.toFloat(), it.height.toFloat())
ratioList = arrayOf(defaultRatio) + ratioList
}
cropOption.apply {
setAspectRatioOptions(
0,
*ratioList.take(5).toTypedArray()
)
}
}
}
private fun getNotSupportCrop(): Array {
return arrayOf(PictureMimeType.ofGIF(), PictureMimeType.ofWEBP())
}
private fun setEditMediaEvent(): OnMediaEditInterceptListener {
return MediaEditInterceptListener(getSandboxPath(appContext), cropOption)
}
private fun setStyle() {
val primaryColor = ColorPropConverter.getColor(config.primaryColor, appContext)
val isNumber =
config.selectMode == SelectMode.MULTIPLE && config.selectBoxStyle == SelectBoxStyle.NUMBER
val selectType = if (isNumber) R.drawable.picture_selector else R.drawable.checkbox_selector
val isDark = config.theme == Theme.DARK
val backgroundDark = ColorPropConverter.getColor(config.backgroundDark, appContext)
?: ContextCompat.getColor(
appContext, com.luck.picture.lib.R.color.ps_color_33
)
val foreground = if (isDark) Color.WHITE else Color.BLACK
val background = if (isDark) backgroundDark else Color.WHITE
val titleBar = TitleBarStyle()
val bottomBar = BottomNavBarStyle()
val mainStyle = SelectMainStyle()
val iconBack =
if (isDark) com.luck.picture.lib.R.drawable.ps_ic_back else com.luck.picture.lib.R.drawable.ps_ic_black_back
cropOption.setLogoColor(primaryColor ?: Color.BLACK)
// TITLE BAR
titleBar.titleBackgroundColor = background
titleBar.isAlbumTitleRelativeLeft = true
titleBar.titleAlbumBackgroundResource = com.luck.picture.lib.R.drawable.ps_album_bg
titleBar.titleDrawableRightResource = com.luck.picture.lib.R.drawable.ps_ic_grey_arrow
titleBar.previewTitleLeftBackResource = iconBack
titleBar.titleLeftBackResource = iconBack
titleBar.isHideCancelButton = true
// BOTTOM BAR
bottomBar.bottomPreviewNormalTextColor = foreground
bottomBar.bottomPreviewSelectTextColor = foreground
bottomBar.bottomNarBarBackgroundColor = background
bottomBar.bottomEditorTextColor = foreground
bottomBar.bottomOriginalTextColor = foreground
bottomBar.bottomPreviewNarBarBackgroundColor = background
mainStyle.mainListBackgroundColor = foreground
mainStyle.selectNormalTextColor = foreground
mainStyle.isDarkStatusBarBlack = !isDark
mainStyle.statusBarColor = background
mainStyle.mainListBackgroundColor = background
mainStyle.adapterPreviewGalleryItemSize = DensityUtil.dip2px(appContext, 52f);
mainStyle.adapterPreviewGalleryBackgroundResource =
if (isDark) com.luck.picture.lib.R.drawable.ps_preview_gallery_bg else R.drawable.preview_gallery_white_bg
mainStyle.adapterPreviewGalleryFrameResource = R.drawable.preview_gallery_item
mainStyle.previewBackgroundColor = background
bottomBar.isCompleteCountTips = false
bottomBar.bottomOriginalTextSize = Constant.TOOLBAR_TEXT_SIZE
bottomBar.bottomSelectNumTextSize = Constant.TOOLBAR_TEXT_SIZE
// bottomBar.bottomPreviewNormalTextSize = Constant.TOOLBAR_TEXT_SIZE
// bottomBar.bottomEditorTextSize = Constant.TOOLBAR_TEXT_SIZE
// MAIN STYLE
mainStyle.isCompleteSelectRelativeTop = false
mainStyle.isPreviewDisplaySelectGallery = true
mainStyle.isAdapterItemIncludeEdge = true
mainStyle.isPreviewSelectRelativeBottom = false
// mainStyle.previewSelectTextSize = Constant.TOOLBAR_TEXT_SIZE
mainStyle.selectTextColor = primaryColor ?: Color.BLACK
// mainStyle.selectTextSize = Constant.TOOLBAR_TEXT_SIZE
mainStyle.selectBackground = selectType
mainStyle.isSelectNumberStyle = isNumber
mainStyle.previewSelectBackground = selectType
mainStyle.isPreviewSelectNumberStyle = isNumber
if (config.camera != null) {
// hide title camera
mainStyle.adapterCameraText = " "
}
// custom toolbar text
config.text.let { text ->
text?.finish.let {
mainStyle.selectText = it
mainStyle.selectNormalText = it
mainStyle.selectText = it
}
text?.preview.let {
mainStyle.previewSelectText = it
}
text?.original.let {
bottomBar.bottomOriginalText = it
}
text?.edit.let {
bottomBar.bottomEditorText = it
}
}
// SET STYLE
style.titleBarStyle = titleBar
style.bottomBarStyle = bottomBar
style.selectMainStyle = mainStyle
// ANIMATION SLIDE FROM BOTTOM
val animationStyle = PictureWindowAnimationStyle()
animationStyle.setActivityEnterAnimation(com.luck.picture.lib.R.anim.ps_anim_up_in)
animationStyle.setActivityExitAnimation(com.luck.picture.lib.R.anim.ps_anim_down_out)
style.windowAnimationStyle = animationStyle
}
private fun handleSelectedAssets(config: NitroConfig) {
val assets = config.selectedAssets
val assetIds = assets.map { it.localIdentifier }.toSet()
dataList = dataList.filter { media ->
assetIds.contains(media.id.toString())
}.toMutableList()
}
private fun getResult(item: LocalMedia): PickerResult {
val type: ResultType =
if (item.mimeType.startsWith("video/")) ResultType.VIDEO else ResultType.IMAGE
var path = item.path
var width: Double = item.width.toDouble()
var height: Double = item.height.toDouble()
val thumbnail = item.videoThumbnailPath?.let {
if (!it.startsWith("file://")) "file://$it" else it
}
if (item.isCut) {
path = "file://${item.cutPath}"
width = item.cropImageWidth.toDouble()
height = item.cropImageHeight.toDouble()
}
if (!path.startsWith("file://") && !path.startsWith("content://") && type == ResultType.IMAGE)
path = "file://$path"
val media = PickerResult(
localIdentifier = item.id.toString(),
width,
height,
mime = item.mimeType,
size = item.size.toDouble(),
bucketId = item.bucketId.toDouble(),
realPath = item.realPath,
parentFolderName = item.parentFolderName,
creationDate = item.dateAddedTime.toDouble(),
crop = item.isCut,
path,
type,
fileName = item.fileName,
thumbnail = thumbnail,
duration = item.duration.toDouble()
)
return media
}
override fun getAppContext(): Context {
return reactApplicationContext
}
override fun getPictureSelectorEngine(): PictureSelectorEngine {
return PictureSelectorEngineImp()
}
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/MultipleImagePickerPackage.java
================================================
package com.margelo.nitro.multipleimagepicker;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.TurboReactPackage;
import java.util.HashMap;
public class MultipleImagePickerPackage extends TurboReactPackage {
@Nullable
@Override
public NativeModule getModule(@NonNull String name, @NonNull ReactApplicationContext reactContext) {
return null;
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return HashMap::new;
}
static {
System.loadLibrary("MultipleImagePicker");
}
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/PictureSelectorEngineImp.kt
================================================
package com.margelo.nitro.multipleimagepicker
import android.util.Log
import com.luck.picture.lib.basic.IBridgeLoaderFactory
import com.luck.picture.lib.engine.*
import com.luck.picture.lib.engine.CompressEngine
import com.luck.picture.lib.entity.LocalMedia
import com.luck.picture.lib.interfaces.OnInjectLayoutResourceListener
import com.luck.picture.lib.interfaces.OnResultCallbackListener
class PictureSelectorEngineImp : PictureSelectorEngine {
/**
* 重新创建[ImageEngine]引擎
*
* @return
*/
override fun createImageLoaderEngine(): ImageEngine {
return GlideEngine.createGlideEngine()
}
/**
* 重新创建[CompressEngine]引擎
*
* @return
*/
override fun createCompressEngine(): CompressEngine? {
// TODO 这种情况是内存极度不足的情况下,比如开启开发者选项中的不保留活动或后台进程限制,导致CompressEngine被回收
return null
}
/**
* 重新创建[CompressEngine]引擎
*
* @return
*/
override fun createCompressFileEngine(): CompressFileEngine? {
// TODO 这种情况是内存极度不足的情况下,比如开启开发者选项中的不保留活动或后台进程限制,导致CompressFileEngine被回收
return null
}
/**
* 重新创建[ExtendLoaderEngine]引擎
*
* @return
*/
override fun createLoaderDataEngine(): ExtendLoaderEngine? {
return null
}
override fun createVideoPlayerEngine(): VideoPlayerEngine<*>? {
return null
}
override fun onCreateLoader(): IBridgeLoaderFactory? {
return null
}
/**
* 重新创建[SandboxFileEngine]引擎
*
* @return
*/
override fun createSandboxFileEngine(): SandboxFileEngine? {
return null
}
override fun createUriToFileTransformEngine(): UriToFileTransformEngine? {
return null
}
override fun createLayoutResourceListener(): OnInjectLayoutResourceListener? {
return null
}
override fun getResultCallbackListener(): OnResultCallbackListener {
return object : OnResultCallbackListener {
override fun onResult(result: ArrayList) {
Log.i(TAG, "onResult:" + result.size)
}
override fun onCancel() {
Log.i(TAG, "PictureSelector onCancel")
}
}
}
companion object {
private val TAG = PictureSelectorEngineImp::class.java.simpleName
}
}
================================================
FILE: android/src/main/java/com/margelo/nitro/multipleimagepicker/VideoThumbnailEngine.kt
================================================
package com.margelo.nitro.multipleimagepicker
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.luck.picture.lib.interfaces.OnKeyValueResultCallbackListener
import com.luck.picture.lib.interfaces.OnVideoThumbnailEventListener
import com.luck.picture.lib.utils.PictureFileUtils
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
class VideoThumbnailEngine(private val targetPath: String) : OnVideoThumbnailEventListener {
override fun onVideoThumbnail(
context: Context, videoPath: String, call: OnKeyValueResultCallbackListener
) {
Glide.with(context).asBitmap().sizeMultiplier(0.6f).load(videoPath)
.into(object : CustomTarget() {
override fun onResourceReady(
resource: Bitmap, transition: Transition?
) {
val stream = ByteArrayOutputStream()
resource.compress(Bitmap.CompressFormat.JPEG, 60, stream)
var fos: FileOutputStream? = null
var result: String? = null
try {
val targetFile =
File(targetPath, "thumbnails_" + System.currentTimeMillis() + ".jpg")
fos = FileOutputStream(targetFile)
fos.write(stream.toByteArray())
fos.flush()
result = targetFile.absolutePath
} catch (e: IOException) {
e.printStackTrace()
} finally {
PictureFileUtils.close(fos)
PictureFileUtils.close(stream)
}
call.onCallback(videoPath, result)
}
override fun onLoadCleared(placeholder: Drawable?) {
call.onCallback(videoPath, "")
}
})
}
}
================================================
FILE: android/src/main/res/anim/anim_modal_in.xml
================================================
================================================
FILE: android/src/main/res/drawable/checkbox_selector.xml
================================================
================================================
FILE: android/src/main/res/drawable/complete_button.xml
================================================
================================================
FILE: android/src/main/res/drawable/ic_checkmark.xml
================================================
================================================
FILE: android/src/main/res/drawable/ic_down.xml
================================================
================================================
FILE: android/src/main/res/drawable/num_oval.xml
================================================
================================================
FILE: android/src/main/res/drawable/picture_not_selected.xml
================================================
================================================
FILE: android/src/main/res/drawable/picture_selector.xml
================================================
================================================
FILE: android/src/main/res/drawable/preview_gallery_item.xml
================================================
================================================
FILE: android/src/main/res/drawable/preview_gallery_white_bg.xml
================================================
================================================
FILE: android/src/main/res/layout/loading_dialog.xml
================================================
================================================
FILE: android/src/main/res/values/colors.xml
================================================
#393a3e#000000#f6f6f6#fafafa#B6B6B6#f94c51#43c117#53575e#00000000#FFFFFF#E0DBDBDB#7D7DFF#9b9b9b#E0FF6100#FF0000#FB9300#333333#0D333333
================================================
FILE: android/src/main/res/values/styles.xml
================================================
================================================
FILE: babel.config.js
================================================
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
};
================================================
FILE: docs/.gitignore
================================================
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: docs/docs/CAMERA.mdx
================================================
---
id: camera
title: Camera 📸
sidebar_label: Camera 📸
slug: /camera
---
import ReactPlayer from 'react-player'
The camera module provides a simple interface for capturing photos and recording videos with customizable options.
## Usage
```typescript
import { openCamera } from '@baronha/react-native-multiple-image-picker'
const open = async () => {
try {
const response = await openCamera({
mediaType: 'all',
cameraDevice: 'back'
})
console.log(response)
} catch (e) {
console.log(e)
}
}
```
## Configuration Options
### `mediaType`
Specifies the type of media that can be captured.
```typescript
mediaType?: 'video' | 'image' | 'all'
```
**Default:** `'all'`
### `cameraDevice`
Selects which camera to use for capture.
```typescript
cameraDevice?: 'front' | 'back'
```
**Default:** `'back'`
### `videoMaximumDuration`
Sets the maximum duration for video recording in seconds.
```typescript
videoMaximumDuration?: number
```
**Default:** No limit
### `presentation`
Controls how the camera view is presented (iOS only).
```typescript
presentation?: 'fullScreenModal' | 'formSheet'
```
**Default:** `'fullScreenModal'`
### `language`
Sets the interface language.
```typescript
language?: Language
```
**Supported values:**
- 🌐 `'system'`: System default
- 🇨🇳 `'zh-Hans'`: Simplified Chinese
- 🇹🇼 `'zh-Hant'`: Traditional Chinese
- 🇯🇵 `'ja'`: Japanese
- 🇰🇷 `'ko'`: Korean
- 🇬🇧 `'en'`: English
- 🇹🇭 `'th'`: Thai
- 🇮🇩 `'id'`: Indonesian
- 🇻🇳 `'vi'`: Vietnamese
- 🇷🇺 `'ru'`: Russian
- 🇩🇪 `'de'`: German
- 🇫🇷 `'fr'`: French
- 🇸🇦 `'ar'`: Arabic
**Default:** `'system'`
### `crop`
Enables and configures image cropping after capture.
See details in [Crop Configuration](/config/#crop-)
### `color`
- **Type**: [**ColorValue**](https://reactnative.dev/docs/colors)
- **Default**: 🟦 `#2979ff`
- **Required**: No
- **Platform**: iOS, Android
## Result Type
The camera function returns a `CameraResult` object:
```typescript
interface CameraResult {
/**
* Path to the captured media file
* - iOS: Starts with 'file://'
* - Android: Can start with 'file://' or 'content://'
*/
path: string
/**
* Type of captured media
* - 'video': For video recordings
* - 'image': For photos
*/
type: 'video' | 'image'
/**
* Width of the media in pixels
* For cropped images, this represents the final cropped width
*/
width: number
/**
* Height of the media in pixels
* For cropped images, this represents the final cropped height
*/
height: number
/**
* Duration of the video in seconds
* Only available when type is 'video'
* @platform ios, android
*/
duration: number
/**
* Path to the video thumbnail image
* Only available when type is 'video'
* Format: 'file://path/to/thumbnail.jpg'
* @platform ios, android
*/
thumbnail?: string
/**
* Original filename of the captured media
* Example: 'IMG_1234.JPG' or 'VID_5678.MP4'
*/
fileName: string
}
```
### Example Response
#### Photo Capture
```typescript
{
path: 'file:///var/mobile/Containers/.../IMG_0123.JPG',
type: 'image',
width: 3024,
height: 4032,
fileName: 'IMG_0123.JPG'
}
```
#### Video Recording
```typescript
{
path: 'file:///var/mobile/Containers/.../VID_0124.MP4',
type: 'video',
width: 1920,
height: 1080,
duration: 15.6,
thumbnail: 'file:///var/mobile/Containers/.../VID_0124_thumb.JPG',
fileName: 'VID_0124.MP4'
}
```
### Notes
- The `path` format may vary between iOS and Android. Always use the provided path as-is.
- Video thumbnails are automatically generated and provided in the `thumbnail` property.
- For cropped images, the `width` and `height` reflect the dimensions after cropping.
- The `duration` property is only available for video recordings and is measured in seconds.
- All file paths are provided with the appropriate prefix (`file://` or `content://`).
## Examples
### Photo Capture
```typescript
const result = await openCamera({
mediaType: 'image',
cameraDevice: 'back'
})
```
### Video Recording
```typescript
const result = await openCamera({
mediaType: 'video',
videoMaximumDuration: 30,
cameraDevice: 'front'
})
```
### With Cropping
```typescript
const result = await openCamera({
mediaType: 'image',
crop: {
circle: true,
ratio: [
{ title: "Square", width: 1, height: 1 },
{ title: "Portrait", width: 3, height: 4 }
]
}
})
```
### Custom UI
```typescript
const result = await openCamera({
color: '#FF6B6B',
language: 'en',
presentation: 'fullScreenModal'
})
```
## Platform Specific Notes
### iOS
- Supports `presentation` option for modal style
- Full support for all UI customization options
### Android
- Maximum 4 custom crop ratios
- Some UI elements may appear differently
## Required Permissions
### iOS
Add to `Info.plist`:
```xml
NSCameraUsageDescriptionCamera access is required to take photos and videosNSMicrophoneUsageDescriptionMicrophone access is required to record videos
```
### Android
Add to `AndroidManifest.xml`:
```xml
```
================================================
FILE: docs/docs/CONFIG.mdx
================================================
---
id: config
title: Configuration
sidebar_label: Configuration
slug: /config
---
# Configuration
### `selectMode`
Mode of selection in the picker.
- **Type**: string
- **Default**: `multiple`
- **Required**: No
- **Options**:
- `single`: Single selection mode
- `multiple`: Multiple selection mode
### `selectedAssets`
Keep previous selection in Multiple Image Picker
See [**Result**](/result)
- **Type**: Result[]
- **Default**: `[]`
- **Required**: No
### `allowedLimit`
Display additional select more media when permission on `iOS` is limited.
- **Type**: boolean
- **Default**: `true`
- **Required**: No
- **Platform**: iOS
### `allowSwipeToSelect`
Allow swiping to select items.
- **Type**: boolean
- **Default**: `true`
- **Required**: No
- **Platform**: iOS, Android
### `isHiddenOriginalButton`
Hide the original button.
- **Type**: boolean
- **Default**: `false`
- **Required**: No
- **Platform**: iOS, Android
### `maxSelect`
Maximum number of items that can be selected.
- **Type**: number
- **Default**: `20`
- **Required**: No
- **Platform**: iOS, Android
### `maxVideo`
Maximum number of videos allowed.
- **Type**: number
- **Default**: `20`
- **Required**: No
- **Platform**: iOS, Android
## Camera 📸
Configuration camera functionality.
- **Type**: object
- **Default**: `{}`
- **Required**: No
- **Platform**: iOS, Android
### `cameraDevice`
Camera device to be used.
- **Type**: `string`
- **Default**: `back`
- **Required**: No
- **Platform**: iOS
- **Options**:
- `back`: Back camera
- `front`: Front camera
### `videoMaximumDuration`
Maximum video duration in seconds.
- **Type**: number
- **Default**: 60
- **Required**: No
- **Platform**: iOS, Android
## Crop 🪚
Configuration for image cropping functionality.
- **Type**: object
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
### `circle`
Enable circular crop mask.
- **Type**: boolean
- **Default**: `false`
- **Required**: No
- **Platform**: iOS, Android
### `ratio`
Aspect ratios for cropping.
Android: Maximum: 4 items
- **Type**: `array`
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
- **Properties**:
- `title`: string - Display title for the ratio (e.g., "Square", "16:9")
- `width`: number - Width value for the aspect ratio
- `height`: number - Height value for the aspect ratio
### `defaultRatio`
Default ratio to be selected when opening the crop interface.
- **Type**: `object`
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
- **Properties**:
- `title`: string - Display title for the ratio (e.g., "Square", "16:9")
- `width`: number - Width value for the aspect ratio
- `height`: number - Height value for the aspect ratio
### `freeStyle`
Enable free style cropping.
- **Type**: `boolean`
- **Default**: `false`
- **Required**: No
- **Platform**: iOS, Android
---
## UI Customize 🌈
### `theme`
Theme mode for the picker.
- **Type**: string
- **Default**: `system`
- **Required**: No
- **Platform**: iOS, Android
- **Options**:
- `light`: Light theme
- `dark`: Dark theme
- `system`: System default theme
### `primaryColor`
Primary color for the picker UI elements.
- **Type**: [**ColorValue**](https://reactnative.dev/docs/colors)
- **Default**: 🟦 `#2979ff`
- **Required**: No
- **Platform**: iOS, Android
### `backgroundDark`
Background color for dark mode UI elements.
- **Type**: [**ColorValue**](https://reactnative.dev/docs/colors)
- **Default**: ⚫️ `#1A1A1A`
- **Required**: No
- **Platform**: iOS, Android
### `selectBoxStyle`
Style of selection box in the picker.
- **Type**: string
- **Default**: `number`
- **Required**: No
- **Options**:
- `number`: Show numbers in selection box
- `tick`: Show checkmark in selection box
### `spacing`
Spacing between items in the grid.
- **Type**: number
- **Default**: `2`
- **Required**: No
- **Platform**: iOS, Android
### `numberOfColumn`
Number of columns in the grid view.
- **Type**: number
- **Default**: `4`
- **Required**: No
- **Platform**: iOS, Android
### `presentation`
Modal presentation style for the picker.
- **Type**: string
- **Default**: `fullScreenModal`
- **Required**: No
- **Platform**: iOS
- **Options**:
- `fullScreenModal`: Full screen presentation
- `formSheet`: Form sheet presentation
---
## Filter data 🎞️
### `mediaType`
Type of media to be displayed in the picker.
- **Type**: string
- **Default**: `all`
- **Required**: No
- **Options**:
- `video`: Only show videos
- `image`: Only show images
- `all`: Show both videos and images
### `maxVideoDuration`
Maximum duration for videos in seconds.
- **Type**: number
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
### `minVideoDuration`
Minimum duration for videos in seconds.
- **Type**: number
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
### `maxFileSize`
Maximum file size in bytes.
- **Type**: number
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
---
## Preview 🌠
### `isPreview`
Enable preview functionality.
- **Type**: boolean
- **Default**: `true`
- **Required**: No
- **Platform**: iOS, Android
### `isShowPreviewList`
Show preview list.
- **Type**: boolean
- **Default**: `false`
- **Required**: No
- **Platform**: iOS
### `isHiddenPreviewButton`
Hide the preview button and button mode.
- **Type**: boolean
- **Default**: `false`
- **Required**: No
- **Platform**: iOS, Android
### `allowHapticTouchPreview`
Enable haptic feedback on preview.
- **Type**: boolean
- **Default**: `false`
- **Required**: No
- **Platform**: iOS
---
## Localization 🌐
### `text`
Custom text labels for buttons and headers.
- **Type**: object
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
- **Properties**:
- `finish`: string - Text for finish/done button
- `original`: string - Text for original button
- `preview`: string - Text for preview button
- `edit`: string - Text for edit button
### `language`
Interface language for the picker.
- **Type**: string
- **Default**: `system`
- **Required**: No
- **Options**:
- `system`: 🌐 System default
- `zh-Hans`: 🇨🇳 Simplified Chinese
- `zh-Hant`: 🇹🇼 Traditional Chinese
- `ja`: 🇯🇵 Japanese
- `ko`: 🇰🇷 Korean
- `en`: 🇬🇧 English
- `vi`: 🇻🇳 Vietnamese
- `ru`: 🇷🇺 Russian
- `de`: 🇩🇪 German
- `fr`: 🇫🇷 French
- `ar`: 🇸🇦 Arabic
**iOS Only**:
- `id`: 🇮🇩 Indonesian
- `th`: 🇹🇭 Thai
================================================
FILE: docs/docs/CROP.mdx
================================================
---
id: crop
title: Crop 🍕
sidebar_label: Crop 🍕
slug: /crop
---
## Usage
```typescript
import { openCropper } from '@baronha/react-native-multiple-image-picker'
const cropConfig: CropConfig = {
// ...
}
const open = async () => {
try {
const response = await openCropper('file://path/to/image.jpg', cropConfig)
setImages(response)
} catch (e) {
// catch error for multiple image picker
}
}
```
## CropConfig
### `circle`
Enable circular crop mask.
- **Type**: boolean
- **Default**: `false`
- **Required**: No
- **Platform**: iOS, Android
### `ratio`
Aspect ratios for cropping.
Android: Maximum: 4 items
- **Type**: `array`
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
- **Properties**:
- `title`: string - Display title for the ratio (e.g., "Square", "16:9")
- `width`: number - Width value for the aspect ratio
- `height`: number - Height value for the aspect ratio
### `defaultRatio`
Default ratio to be selected when opening the crop interface.
- **Type**: `object`
- **Default**: `undefined`
- **Required**: No
- **Platform**: iOS, Android
- **Properties**:
- `title`: string - Display title for the ratio (e.g., "Square", "16:9")
- `width`: number - Width value for the aspect ratio
- `height`: number - Height value for the aspect ratio
### `freeStyle`
Enable free style cropping.
- **Type**: `boolean`
- **Default**: `false`
- **Required**: No
- **Platform**: iOS, Android
### `language`
- **Type**: `string`
- **Default**: `false`
- **Required**: No
- **Platform**: iOS
See [**Language**](/config/#language)
## Result
### `path`
- **Type**: `string`
### `width`
- **Type**: `number`
### `height`
- **Type**: `number`
================================================
FILE: docs/docs/GETTING_STARTED.mdx
================================================
---
id: getting-started
title: Getting Started
sidebar_label: Getting Started
sidebar_id: getting-started
slug: /getting-started
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
import useBaseUrl from '@docusaurus/useBaseUrl'
## Requirements
Because RNMIP uses Nitro Module, it complies with Nitro Modules' requirements.
View Nitro Modules' requirements [here](https://nitro.margelo.com/docs/minimum-requirements)
- `iOS` 13+
- `react-native` 0.75+
- `Xcode` 16+
- `react-native` 0.75+
- `compileSdkVersion` 34+
## Installing
Install [@baronha/react-native-multiple-image-picker](https://www.npmjs.com/package/@baronha/react-native-multiple-image-picker) through npm:
> This package requires `react-native-nitro-modules` to be installed first.
> See [react-native-nitro-modules](https://github.com/mrousavy/nitro) for more information.
```bash
yarn add @baronha/react-native-multiple-image-picker
yarn add -D react-native-nitro-modules
cd ios && pod install
```
```bash
npx expo install @baronha/react-native-multiple-image-picker
npx expo install react-native-nitro-modules -- --dev
npx expo prebuild
```
## Updating manifests
### Info.plist
Open your project's `Info.plist` and add the following lines inside the outermost `` tag:
```xml
NSPhotoLibraryAddUsageDescription$(PRODUCT_NAME) needs photo library permissionsNSPhotoLibraryUsageDescription$(PRODUCT_NAME) needs photo library permissionsNSCameraUsageDescription$(PRODUCT_NAME) needs to access your CameraNSMicrophoneUsageDescription$(PRODUCT_NAME) needs to access your microphone so that you can record audio.
```
### Managed Expo
Config your Expo app (`app.json`, `app.config.json` or `app.config.js`):
```json
{
"name": "my app",
"ios": {
// ...
"infoPlist": {
"NSPhotoLibraryAddUsageDescription": "$(PRODUCT_NAME) needs photo library permissions",
"NSPhotoLibraryUsageDescription": "$(PRODUCT_NAME) needs photo library permissions",
// if you allow camera, you need to add this
"NSCameraUsageDescription": "$(PRODUCT_NAME) needs to access your Camera",
"NSMicrophoneUsageDescription": "$(PRODUCT_NAME) needs to access your microphone so that you can record audio"
}
// ...
}
}
```
Finally, compile the mods:
```bash
npx expo prebuild
```
To apply the changes, build a new binary with EAS:
```bash
eas build
```
#### 🎉 Hooray! You're ready to learn about [Usage](/usage)!
---
================================================
FILE: docs/docs/PREVIEW.mdx
================================================
---
id: preview
title: Preview 🎑
sidebar_label: Preview 🎑
slug: /preview
---
import ReactPlayer from 'react-player'
## Overview
Preview is a simple image viewer that supports both local and remote media. It allows you to preview images and videos seamlessly.
## Usage
```typescript
import {
openPreview,
PreviewConfig,
} from '@baronha/react-native-multiple-image-picker'
const previewConfig: PreviewConfig = {
language: 'system',
}
const media: MediaPreview[] = [
// remote image
{
path: 'https://images.unsplash.com/photo-1733235015085-fc29c17c4a16?w=500',
type: 'image',
},
// local video
{
path: 'file://Documents/video-sample/mp4',
thumbnail:
'https://images.unsplash.com/photo-1733889886752-f4599c954608?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
type: 'video',
},
// remote video
{
path: 'https://cdn.pixabay.com/video/2024/02/09/199958-911694865_large.mp4',
type: 'video',
},
]
// call to preview
openPreview(media, 2, previewConfig)
```
## `MediaPreview[]`
`MediaPreview[]` is an array of media objects that you want to preview. Each object in the array should have the following properties:
**NOTE**: You can also use [`Result[]`](/result) from openPicker's return result
- `path`: A string representing the URL or file path of the media.
- `type`: A string indicating the type of media, either `image` or `video`.
- `thumbnail` (optional): A string representing the URL of the thumbnail image for videos.
- `localIdentifier` (optional): A string representing the local identifier for media from device gallery.
## `index`
default: `0`
The `index` parameter in the `openPreview` function specifies the initial media item to display. It is a zero-based index, meaning that `0` will display the first item in the `MediaPreview` array.
## `previewConfig`
### [`language`](/config/#language)
see [`language`](/config/#language)
### `onLongPress`
`onLongPress` is a callback function that is called when a long press is detected on the media item. It is a function that takes an index as an argument and returns void.
```typescript
onLongPress: (index: number) => void
```
## Additional Information
- Ensure that the media paths are accessible and correctly formatted.
- The `openPreview` function is part of the `@baronha/react-native-multiple-image-picker` package, which should be installed and properly configured in your project.
For more detailed information, refer to the [official documentation](https://github.com/baronha/react-native-multiple-image-picker).
================================================
FILE: docs/docs/RESULT.mdx
================================================
---
id: result
title: Result
sidebar_label: Result
slug: /result
---
The result object returned for each selected media item.
### `path`
- **Type**: string
- **Description**: Local file path of the media
### `fileName`
- **Type**: string
- **Description**: Name of the media file
### `localIdentifier`
- **Type**: string
- **Description**: Unique identifier for the media asset
### `width`
- **Type**: number
- **Description**: Width of the media in pixels
### `height`
- **Type**: number
- **Description**: Height of the media in pixels
### `mime`
- **Type**: string
- **Description**: MIME type of the media file
### `size`
- **Type**: number
- **Description**: File size in bytes
### `bucketId`
- **Type**: number
- **Description**: ID of the bucket containing the media
- **Platform**: Android
### `realPath`
- **Type**: string
- **Description**: Actual file path in the device storage
- **Platform**: Android
### `parentFolderName`
- **Type**: string
- **Description**: Name of the parent folder
- **Platform**: Android
### `creationDate`
- **Type**: number
- **Description**: Creation timestamp of the media
### `type`
- **Type**: string
- **Options**: `image` | `video`
- **Description**: Type of the media file
### `duration`
- **Type**: number
- **Description**: Duration in seconds (for video files)
### `thumbnail`
- **Type**: string
- **Description**: Thumbnail path for video files
### `crop`
- **Type**: boolean
- **Description**: Indicates if the media has been cropped
================================================
FILE: docs/docs/SHOWCASE/index.mdx
================================================
---
id: showcase
title: Showcase ✨
sidebar_label: Showcase ✨
slug: /showcase
---
import style from './showcase.css'
import data from './showcase.json'
List of used applications with `@baronha/react-native-multiple-image-picker`
> Contributions are welcome! If you have an application that uses `@baronha/react-native-multiple-image-picker`
> please open a [**Pull Request**](https://github.com/baronha/react-native-multiple-image-picker/tree/master/docs/docs/SHOWCASE/showcase.json) to add it to the list.