├── .gitignore
├── README.md
├── index.js
├── package.json
└── src
├── components
├── BarContainer.js
├── BottomBar.js
├── CameraScreen.js
├── EdgeSlider.js
├── GridContainer.js
├── OverlayMenu.js
├── Photo.js
├── PhotoBrowser.js
├── PhotoEditor.js
└── Popup.js
└── lib
├── Common.js
├── FileUtil.js
├── Picture.js
├── PictureList.js
├── converters.js
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | .idea/*
4 | npm-debug.*
5 | *.jks
6 | *.p12
7 | *.key
8 | *.mobileprovision
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Image Kit
2 |
3 | ## Information
4 |
5 | Expo provides several basic editing functions, such as resizing and cutting images through the ImageManipulator API.
6 | The purpose of this package is to provide the UI for users to take advantage of this feature.
7 | React Native Image Kit depends on the following packages:
8 |
9 | * Native Base
10 | * Expo
11 | * React Native Progress
12 | * UUID
13 |
14 | For more information, please refer to the "package.json".
15 |
16 | ## Installation
17 |
18 | ````
19 | yarn add react-native-image-kit
20 | ````
21 |
22 | ## Screen Shot
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ## Usage
32 |
33 | ### PhotoEditor
34 |
35 | ````
36 | import { Picture, PhotoEditor } from "react-native-image-kit";
37 |
38 | ...
39 |
40 | _onCloseEditor() {
41 | this._toggleFullScreen(false);
42 | }
43 |
44 | render() {
45 |
46 | let picture = new Picture(uri);
47 |
48 | ...
49 |
50 | return (
51 | ...
52 |
57 | ...
58 | );
59 | }
60 | ````
61 |
62 | ### PhotoBrowser
63 |
64 | ````
65 | import React from 'react';
66 | import { withNavigationFocus } from 'react-navigation';
67 | import { PhotoBrowser, PictureList } from "react-native-image-kit"
68 |
69 |
70 | class Images extends React.Component {
71 |
72 | static navigationOptions = {
73 | headerStyle: {
74 | display: 'none',
75 | },
76 | };
77 |
78 | ...
79 |
80 | _onClose = (pictureList) => {
81 | this.props.navigation.navigate('Home');
82 | pictureList.cleanup();
83 | };
84 |
85 | render() {
86 | let pictureList = new PictureList('images');
87 |
88 |
89 | return (
90 |
94 | );
95 | }
96 | }
97 |
98 | export default withNavigationFocus(Images);
99 |
100 | ````
101 |
102 | ## Components
103 |
104 | ### PhotoEditor
105 |
106 | Simple image editor component. It's editing function depends on the expo.ImageManipulator.
107 |
108 | 1. Properties
109 |
110 | | Prop name | Type | Default value | Description |
111 | | ------------------ | -------- | ------------- | ----------------------------------------------------------------------|
112 | | `style` | Style | `null` | Overrides default container style. |
113 | | `picture` | object | | Required, "Picture" type object. |
114 | | `onClose` | function | | Required, Custom function for closing PhotoEditor. |
115 | | `onDelete` | function | `null` | Custom function to delete image file pointing to "picture" object. |
116 | | `onSpawn` | function | `null` | Receive a Picture object as a parameter and return a new Picture object that stores the current edit state, and you can continue state, and you can continue editing it. The image file that was originally passed to the feature property does not change. For more information, please refer to the _onSpawn() function in "PhotoEditor.js". |
117 | | `onShare` | function | `null` | Receive a Picture object as a parameter and send it to other app. If this property is defined, displays button for sharing photo. |
118 | | `customBtn` | array | `[]` | Array to specify custom buttons to be added to the toolbar. |
119 | | `useCircleProgress`| Boolean | `false` | If true, displays Progress.Circle. (Default : Progress.Bar) |
120 | | `useSpawn` | Boolean | `true` | If true, displays button for spawning photo. If onSpawn is not specified, use the built-in function. |
121 | | `topMargin` | Number | `0` | Distance from the top of the screen to the top of the PhotoEditor component. (by pt) |
122 | | `bottomMargin` | Number | `0` | Distance from the bottom of the PhotoEditor component to the bottom of the screen. (by pt) |
123 |
124 | 2. Setter
125 |
126 | | Prop name | Type | Default value | Description |
127 | | ------------------ | -------- | ------------- | ----------------------------------------------------------------------|
128 | | customBtn | array | [] | Array to specify custom buttons to be added to the toolbar. |
129 |
130 | 3. Getter
131 |
132 | | Prop name | Type | Default value | Description |
133 | | ------------------ | -------- | ------------- | ----------------------------------------------------------------------|
134 | | customBtn | array | [] | Returns an array that combines the values of the "customBtn" property and the "customBtn" setter. The custom button that is finally added to the toolbar is based on this value. |
135 |
136 | ### PhotoBrowser
137 |
138 | Simple image browser component for predefined folder.
139 | When you click on the thumbnail of the image, PhotoEditor component open it.
140 |
141 | 1. Properties
142 |
143 | | Prop name | Type | Default value | Description |
144 | | ------------------ | -------- | ------------- | ----------------------------------------------------------------------|
145 | | `style` | Style | `null` | Overrides default container style. |
146 | | `isModal` | Boolean | `true` | If true, PhotoBrowser is full screen modal box. |
147 | | `folder` | String | `'images'` | Path to the image folder. THis path shoud be under "expo.FileSystem.documentDirectory" |
148 | | `pictureList` | Object | `null` | "PictureList" type object. If assigned, PhotoBrowser use this this list. Else, the PhotoBrowser will generate a PictureList object from the image files stored in the folder path. |
149 | | `onShare` | function | `null` | To pass sharing function to PhotoBrowser's PhotoEditor component |
150 | | `isModal` | Boolean | `true` | If true, PhotoBrowser is full screen modal box. |
151 | | `useSpawn` | Boolean | `true` | If true, displays button for spawning photo in the PhotoEditor. |
152 | | `usePhotoLib` | Boolean | `true` | If true, displays button for importing photo with the "expo.ImagePicker". |
153 | | `getFromWeb` | Boolean | `true` | If true, displays button for downloading photo from the web. |
154 | | `useCamera` | Boolean | `true` | If true, displays button for accessing camera. |
155 | | `square` | Boolean | `false` | If true, displays the thumbnails as squares. |
156 | | `onClose` | Boolean | `true` | If true, displays button for downloading photo from the web. |
157 | | `orientation` | String | `'auto'` | One of 'auto', 'landscape', 'portrait'. It is effective only when the "isModal" is true. The orientation of the modal box is fixed according to the orientation value. If set to 'auto', use the current orientation of the device. |
158 |
159 | 2. Setter
160 |
161 | | Prop name | Type | Default value | Description |
162 | | ------------------ | -------- | ------------- | ----------------------------------------------------------------------|
163 | | `customBtn` | array | `[]` | Array to specify custom buttons to be added to the toolbar. |
164 | | `customEditorBtn` | array | `[]` | To pass custom button settings to PhotoBrowser's PhotoEditor component|
165 |
166 | 3. Getter
167 |
168 | | Prop name | Type | Default value | Description |
169 | | ------------------ | -------- | ------------- | ----------------------------------------------------------------------|
170 | | `customBtn` | array | `[]` | Returns an array that combines the values of the "customBtn" property and the "customBtn" setter. The custom button that is finally added to the toolbar is based on this value. |
171 | | `customEditorBtn` | array | `[]` | Returns a custom button setting value for the PhotoEditor component called by the PhotoBrowser. |
172 |
173 | ## APIs
174 |
175 | ### Picture
176 |
177 | Class to contain image file URI and edit history. Image file URI should be start with value of
178 | expo.FileSystem.documentDirectory.
179 |
180 | #### constructor(fileUri, width=0, height=0, tempFolderExist=false)
181 |
182 | **Arguments**
183 |
184 | * fileUri (string) -- file:// URI to the image file, or a URI returned by CameraRoll.getPhotos(). (JPG or PNG only)
185 |
186 | * width, height (number) -- The dimensions of the image.
187 |
188 | * tempFolderExist (boolean) -- Whether a temporary folder is created. (for storing history of image manipulation)
189 | If you are not sure, set false.
190 |
191 | **Precautions**
192 |
193 | * If zero is given for the width or height value, the asynchronous function is called internally to obtain this value.
194 |
195 | * If false is given for the tempFolderExist, the asynchronous function is called internally to create the temp folder.
196 |
197 | ### PictureList
198 |
199 | Class to manage image files within a specific folder. The folder URI should be start with value of
200 | expo.FileSystem.documentDirectory.
201 |
202 | #### constructor(folder='images')
203 |
204 | **Arguments**
205 |
206 | * folder (string) -- URI to the image folder. Omit the preceding part equal to the expo.FileSystem.documentDirectory.
207 |
208 | ## Example for custom buttons.
209 |
210 | ````
211 | let buttons = [
212 | {
213 | callback: this._onAction,
214 | bordered: true,
215 | icon: {
216 | name: 'icon name',
217 | type: 'icon type',
218 | },
219 | text: { label: 'action' }
220 | }
221 | ];
222 |
223 | return (
224 |
235 | );
236 | ````
237 |
238 | For the name and type of icon among the examples, see Native Base Package.
239 |
240 | For more information, please refer to the "src/lib/Common.js".
241 |
242 | ## Changelog
243 |
244 | **0.6.0**
245 | - Add camera access.
246 |
247 | **0.5.6**
248 | - Remove some bug.
249 |
250 | **0.5.5**
251 | - Rewrite EdgeSlider component.
252 |
253 | **0.5.4**
254 | - Remove some bug(resize, etc...).
255 | - Add orientation props to Popup component.
256 |
257 | **0.5.3**
258 | - Remove some bug.
259 |
260 | **0.5.2**
261 | - Remove some bug. (Heather Height Issues on Android, Property issues of the PhotoBrowser component)
262 |
263 | **0.5.1**
264 | - Modify readme.md
265 |
266 |
267 | ## Acknowledgement
268 |
269 | * Many part of the React Native Image Kit was adapted from and inspired by Halil Bilir's "React Native Photo Browser".
270 |
271 | * The EdgeSlider component was adapted from and inspired by Tomas Roos's "React Native Multi Slider."
272 |
273 | * The OverlayMenu component was adapted from and inspired by @rt2zz's "React Native Drawer"
274 |
275 | ## License
276 |
277 | **MIT**
278 |
279 | ## Example
280 |
281 | You can see the source of the working app using the react-native-image-kit :
282 | [https://github.com/rheesh/Emarks](https://github.com/rheesh/Emarks)
283 |
284 | ## Remarks
285 |
286 | If you find the react-native-image-kit useful, [please purchase the example app above.](https://play.google.com/store/apps/details?id=com.snac.mdnote)
287 | (App Name : Emarks, Price : 0.99$)
288 |
289 | Or please support me with [a glass of beer :beer:](https://www.paypal.me/SeunghoYi).
290 |
291 |
292 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import PhotoEditor from './src/components/PhotoEditor';
2 | import PhotoBrowser from './src/components/PhotoBrowser';
3 | import Photo from './src/components/Photo';
4 | import Popup from './src/components/Popup';
5 | import OverlayMenu from './src/components/OverlayMenu';
6 | import { FileUtil, Picture, PictureList } from './src/lib';
7 |
8 |
9 | module.exports = {
10 | FileUtil,
11 | Picture,
12 | PictureList,
13 | PhotoBrowser,
14 | PhotoEditor,
15 | Photo,
16 | Popup,
17 | OverlayMenu,
18 | };
19 |
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-image-kit",
3 | "version": "0.6.0",
4 | "description": "react-native package for simple image manipulation (require expo)",
5 | "main": "index.js",
6 | "repository": {
7 | "url": "https://github.com/rheesh/react-native-image-kit",
8 | "type": "git"
9 | },
10 | "author": "Seungho Yi ",
11 | "license": "MIT",
12 | "private": false,
13 | "dependencies": {
14 | "expo": "^31.0.2",
15 | "native-base": "^2.8.1",
16 | "prop-types": "^15.6.2",
17 | "react": "16.5.0",
18 | "react-native": "https://github.com/expo/react-native/archive/sdk-31.0.0.tar.gz",
19 | "react-native-progress": "^3.5.0",
20 | "uuid": "^3.3.2"
21 | },
22 | "keywords": ["image", "image-crop", "image-flip", "image-rotate", "react-native", "expo"],
23 | "bugs": {
24 | "url": "https://github.com/rheesh/react-native-image-kit/issues"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/BarContainer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of BarContainer component
3 | * for bottom toolbar of PhotoEditor.
4 | * This source was adapted from and inspired by Halil Bilir's "React Native Photo Browser".
5 | * @see https://github.com/halilb/react-native-photo-browser
6 | *
7 | * last modified : 2019.01.28
8 | * @module components/BarContainer
9 | * @author Seungho.Yi
10 | * @package react-native-image-kit
11 | * @license MIT
12 | */
13 |
14 | import React, { Component } from 'react';
15 | import PropTypes from 'prop-types';
16 | import {
17 | Animated,
18 | StyleSheet,
19 | ViewPropTypes
20 | } from 'react-native';
21 |
22 | export default class BarContainer extends Component {
23 |
24 | static propTypes = {
25 | style: ViewPropTypes.style,
26 | height: PropTypes.number,
27 | onLayout: PropTypes.func,
28 | children: PropTypes.node,
29 | };
30 |
31 | constructor(props) {
32 | super(props);
33 |
34 | this.state = {
35 | animation: new Animated.Value(1),
36 | };
37 | }
38 |
39 | componentDidUpdate() {
40 | Animated.timing(this.state.animation, {
41 | toValue: 1,
42 | duration: 300,
43 | }).start();
44 | }
45 |
46 | render() {
47 | const { style, children, height, onLayout } = this.props;
48 | return (
49 |
66 | {children}
67 |
68 | );
69 | }
70 | }
71 |
72 | const styles = StyleSheet.create({
73 | container: {
74 | position: 'absolute',
75 | left: 0,
76 | right: 0,
77 | bottom: 0,
78 | borderBottomColor: 'rgba(0, 0, 0, 0.1)',
79 | borderBottomWidth: 1,
80 | backgroundColor: 'rgba(20, 20, 20, 0.5)',
81 | },
82 | });
83 |
84 |
--------------------------------------------------------------------------------
/src/components/BottomBar.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of BottomBar component
3 | * for bottom toolbar of PhotoEditor.
4 | * This source was adapted from and inspired by Halil Bilir's "React Native Photo Browser".
5 | * @see https://github.com/halilb/react-native-photo-browser
6 | *
7 | * last modified : 2019.01.28
8 | * @module components/BottomBar
9 | * @author Seungho.Yi
10 | * @package react-native-image-kit
11 | * @license MIT
12 | */
13 |
14 | import React from 'react';
15 | import PropTypes from 'prop-types';
16 | import { ScrollView, Text, StyleSheet, Dimensions, View } from 'react-native';
17 | import {Button, Icon } from 'native-base';
18 |
19 | import BarContainer from './BarContainer';
20 | import Common from '../lib/Common';
21 |
22 | export default class BottomBar extends React.Component {
23 |
24 | static propTypes = {
25 | height: PropTypes.number,
26 | picture: PropTypes.object,
27 | onResizeByWidth: PropTypes.func,
28 | onResizeByHeight: PropTypes.func,
29 | onCClockwise: PropTypes.func,
30 | onClockwise: PropTypes.func,
31 | onCrop: PropTypes.func,
32 | onVerticalFlip: PropTypes.func,
33 | onHorizontalFlip: PropTypes.func,
34 | onUndo: PropTypes.func,
35 | onReset: PropTypes.func,
36 | customBtn: PropTypes.array,
37 | };
38 |
39 | static defaultProps = {
40 | picture: {},
41 | onResizeByWidth: () => {},
42 | onResizeByHeight: () => {},
43 | onCClockwise: () => {},
44 | onClockwise: () => {},
45 | onCrop: () => {},
46 | onVerticalFlip: () => {},
47 | onHorizontalFlip: () => {},
48 | onUndo: () => {},
49 | onReset: () => {},
50 | customBtn: []
51 | };
52 |
53 | constructor(props) {
54 | super(props);
55 | const {width, height} = Dimensions.get('window');
56 | this.state = {
57 | width: width,
58 | height: height,
59 | }
60 | }
61 |
62 | _onLayout = (e) => {
63 | const scr = Dimensions.get('window');
64 | if ( this.state.width !== scr.width || this.state.height !== scr.height){
65 | this.setState({
66 | width: scr.width,
67 | height: scr.height,
68 | });
69 | }
70 | };
71 |
72 | render() {
73 | const { picture, height, onResizeByWidth, onResizeByHeight,
74 | onCrop, onVerticalFlip, onHorizontalFlip, onUndo,
75 | onReset, onCClockwise, onClockwise } = this.props;
76 | const styles = this.getStyles(this.state.width, this.state.height);
77 | return (
78 |
83 |
85 |
86 | { this.props.customBtn.map( (v, i) =>{
87 | v.key = i;
88 | v.arg = picture;
89 | if(v.icon) v.icon.style = styles.icon;
90 | if(v.text) v.text.style = styles.btnText;
91 | v.style = styles.button;
92 | return Common.button(v);
93 | } ) }
94 |
95 |
96 |
98 |
99 |
103 |
107 |
110 |
113 |
116 |
119 |
122 |
126 |
130 |
131 |
132 |
133 | );
134 | }
135 |
136 | getStyles(w, h) {
137 | return StyleSheet.create({
138 | container: {
139 | flex: 1,
140 | flexDirection: 'column',
141 | },
142 | buttonContainer: {
143 | flex: 1,
144 | borderColor: 'rgba(255, 255, 255, 0.1)',
145 | borderWidth: 1,
146 | },
147 | contentContainer:{
148 | display: 'flex',
149 | flexDirection: 'row',
150 | justifyContent: 'center',
151 | minWidth: w,
152 | },
153 | icon: {
154 | color: '#B0B0B0',
155 | },
156 | clockIcon: {
157 | color: '#B0B0B0',
158 | transform: [{ rotateZ: '90deg'}]
159 | },
160 | mirrorIcon: {
161 | color: '#B0B0B0',
162 | transform: [{ rotateY: '180deg'}]
163 | },
164 | button:{
165 | borderColor: '#606060',
166 | marginLeft: 4,
167 | marginTop: 4,
168 | },
169 | btnText: {
170 | color: '#909090',
171 | marginLeft: -4,
172 | paddingLeft: 0,
173 | marginRight: 8,
174 | },
175 | });
176 | }
177 | }
178 |
179 |
180 |
--------------------------------------------------------------------------------
/src/components/CameraScreen.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of CameraScreen component
3 | * for taking picture from camera.
4 | * This source was adapted from and inspired by expo Camera component example.
5 | * @see https://github.com/expo/camerja
6 | *
7 | * last modified : 2019.03.03
8 | * @module components/CameraScreen
9 | * @author Seungho.Yi
10 | * @package react-native-image-kit
11 | * @license MIT
12 | */
13 |
14 | import { Constants, Camera } from 'expo';
15 | import React from 'react';
16 | import { StyleSheet, Text, View, TouchableOpacity, Platform, Dimensions,} from 'react-native';
17 | import { Icon } from 'native-base';
18 | import PropTypes from "prop-types";
19 |
20 | let isIPhoneX = false;
21 |
22 | const landmarkSize = 2;
23 |
24 | const flashModeOrder = {
25 | off: 'on',
26 | on: 'auto',
27 | auto: 'torch',
28 | torch: 'off',
29 | };
30 |
31 | const flashIcons = {
32 | off: 'flash-off',
33 | on: 'flash-on',
34 | auto: 'flash-auto',
35 | torch: 'highlight'
36 | };
37 |
38 | const wbOrder = {
39 | auto: 'sunny',
40 | sunny: 'cloudy',
41 | cloudy: 'shadow',
42 | shadow: 'fluorescent',
43 | fluorescent: 'incandescent',
44 | incandescent: 'auto',
45 | };
46 |
47 | const wbIcons = {
48 | auto: 'wb-auto',
49 | sunny: 'wb-sunny',
50 | cloudy: 'wb-cloudy',
51 | shadow: 'beach-access',
52 | fluorescent: 'wb-iridescent',
53 | incandescent: 'wb-incandescent',
54 | };
55 |
56 | export default class CameraScreen extends React.Component {
57 |
58 | static propTypes = {
59 | width: PropTypes.number,
60 | onCancel: PropTypes.func,
61 | onTakePhoto: PropTypes.func,
62 | };
63 |
64 | static defaultProps = {
65 | width: 0,
66 | onCancel: null,
67 | onTakePhoto: null,
68 | };
69 |
70 | constructor(props) {
71 | super(props);
72 | if (props.width){
73 | this.width = props.width;
74 | }else{
75 | const { width } = Dimensions.get('window');
76 | this.width = width;
77 | }
78 | this.state = {
79 | flash: 'off',
80 | zoom: 0,
81 | autoFocus: 'on',
82 | type: 'back',
83 | whiteBalance: 'auto',
84 | ratio: '16:9',
85 | ratios: [],
86 | faceDetecting: false,
87 | faces: [],
88 | newPhotos: false,
89 | permissionsGranted: true,
90 | pictureSize: undefined,
91 | pictureSizes: [],
92 | pictureSizeId: 0,
93 | showMoreOptions: false,
94 | };
95 | }
96 | /*
97 | async componentWillMount() {
98 | const { status } = await Permissions.askAsync(Permissions.CAMERA);
99 | const ratios = await this.camera.getSupportedRatiosAsync();
100 | this.setState({
101 | permissionsGranted: status === 'granted',
102 | ratios
103 | });
104 | }
105 | */
106 | componentWillReceiveProps(nextProps, nextContext) {
107 | if (nextProps.width){
108 | this.width = nextProps.width;
109 | }else{
110 | const { width } = Dimensions.get('window');
111 | this.width = width;
112 | }
113 | }
114 |
115 | toggleMoreOptions = () => this.setState({ showMoreOptions: !this.state.showMoreOptions });
116 |
117 | toggleFacing = () => this.setState({ type: this.state.type === 'back' ? 'front' : 'back' });
118 |
119 | toggleFlash = () => this.setState({ flash: flashModeOrder[this.state.flash] });
120 |
121 | setRatio = ratio => this.setState({ ratio });
122 |
123 | toggleWB = () => this.setState({ whiteBalance: wbOrder[this.state.whiteBalance] });
124 |
125 | toggleFocus = () => this.setState({ autoFocus: this.state.autoFocus === 'on' ? 'off' : 'on' });
126 |
127 | zoomOut = () => this.setState({ zoom: this.state.zoom - 0.1 < 0 ? 0 : this.state.zoom - 0.1 });
128 |
129 | zoomIn = () => this.setState({ zoom: this.state.zoom + 0.1 > 1 ? 1 : this.state.zoom + 0.1 });
130 |
131 | setFocusDepth = depth => this.setState({ depth });
132 |
133 | toggleFaceDetection = () => this.setState({ faceDetecting: !this.state.faceDetecting });
134 |
135 | onPictureSaved = ( photo ) => {
136 | if(this.props.onTakePhoto) return this.props.onTakePhoto(photo);
137 | };
138 |
139 | onCancel = () => {
140 | if(this.props.onCancel) return this.props.onCancel();
141 | };
142 |
143 | takePicture = () => {
144 | if (this.camera) {
145 | this.camera.takePictureAsync({ onPictureSaved: this.onPictureSaved });
146 | }
147 | };
148 |
149 | handleMountError = ({ message }) => console.error(message);
150 |
151 | onFacesDetected = ({ faces }) => this.setState({ faces });
152 | onFaceDetectionError = state => console.warn('Faces detection error:', state);
153 |
154 | collectPictureSizes = async () => {
155 | if (this.camera) {
156 | const pictureSizes = await this.camera.getAvailablePictureSizesAsync(this.state.ratio);
157 | let pictureSizeId = 0;
158 | if (Platform.OS === 'ios') {
159 | pictureSizeId = pictureSizes.indexOf('High');
160 | } else {
161 | // returned array is sorted in ascending order - default size is the largest one
162 | pictureSizeId = pictureSizes.length-1;
163 | }
164 | this.setState({ pictureSizes, pictureSizeId, pictureSize: pictureSizes[pictureSizeId] });
165 | }
166 | };
167 |
168 | previousPictureSize = () => this.changePictureSize(1);
169 | nextPictureSize = () => this.changePictureSize(-1);
170 |
171 | changePictureSize = direction => {
172 | let newId = this.state.pictureSizeId + direction;
173 | const length = this.state.pictureSizes.length;
174 | if (newId >= length) {
175 | newId = 0;
176 | } else if (newId < 0) {
177 | newId = length -1;
178 | }
179 | this.setState({ pictureSize: this.state.pictureSizes[newId], pictureSizeId: newId });
180 | };
181 |
182 | renderFace({ bounds, faceID, rollAngle, yawAngle }) {
183 | return (
184 |
199 | ID: {faceID}
200 | rollAngle: {rollAngle.toFixed(0)}
201 | yawAngle: {yawAngle.toFixed(0)}
202 |
203 | );
204 | }
205 |
206 | renderLandmarksOfFace(face) {
207 | const renderLandmark = position =>
208 | position && (
209 |
218 | );
219 | return (
220 |
221 | {renderLandmark(face.leftEyePosition)}
222 | {renderLandmark(face.rightEyePosition)}
223 | {renderLandmark(face.leftEarPosition)}
224 | {renderLandmark(face.rightEarPosition)}
225 | {renderLandmark(face.leftCheekPosition)}
226 | {renderLandmark(face.rightCheekPosition)}
227 | {renderLandmark(face.leftMouthPosition)}
228 | {renderLandmark(face.mouthPosition)}
229 | {renderLandmark(face.rightMouthPosition)}
230 | {renderLandmark(face.noseBasePosition)}
231 | {renderLandmark(face.bottomMouthPosition)}
232 |
233 | );
234 | }
235 |
236 | renderFaces = () =>
237 |
238 | {this.state.faces.map(this.renderFace)}
239 | ;
240 |
241 | renderLandmarks = () =>
242 |
243 | {this.state.faces.map(this.renderLandmarksOfFace)}
244 | ;
245 |
246 | renderNoPermissions = () =>
247 |
248 |
249 | Camera permissions not granted - cannot open camera preview.
250 |
251 | ;
252 |
253 | renderTopBar = () =>
254 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 | AF
267 |
268 | ;
269 |
270 | renderBottomBar = () =>
271 |
273 |
274 |
275 |
276 |
277 |
281 |
282 |
283 |
284 |
285 |
286 |
287 | ;
288 |
289 | renderMoreOptions = () =>
290 | (
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 | Picture quality
299 |
300 |
301 |
302 |
303 |
304 | {this.state.pictureSize}
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 | );
313 |
314 | renderCamera = () =>
315 | (
316 |
317 | {
319 | this.camera = ref;
320 | }}
321 | style={styles.camera}
322 | onCameraReady={this.collectPictureSizes}
323 | type={this.state.type}
324 | flashMode={this.state.flash}
325 | autoFocus={this.state.autoFocus}
326 | zoom={this.state.zoom}
327 | whiteBalance={this.state.whiteBalance}
328 | ratio={this.state.ratio}
329 | pictureSize={this.state.pictureSize}
330 | onMountError={this.handleMountError}
331 | onFacesDetected={this.state.faceDetecting ? this.onFacesDetected : undefined}
332 | onFaceDetectionError={this.onFaceDetectionError}
333 | >
334 | {this.renderTopBar()}
335 | {this.renderBottomBar()}
336 |
337 | {this.state.faceDetecting && this.renderFaces()}
338 | {this.state.faceDetecting && this.renderLandmarks()}
339 | {this.state.showMoreOptions && this.renderMoreOptions()}
340 |
341 | );
342 |
343 | render() {
344 | const cameraScreenContent = this.state.permissionsGranted
345 | ? this.renderCamera()
346 | : this.renderNoPermissions();
347 | return {cameraScreenContent};
348 | }
349 | }
350 |
351 | const styles = StyleSheet.create({
352 | container: {
353 | flex: 1,
354 | backgroundColor: '#000',
355 | },
356 | camera: {
357 | flex: 1,
358 | justifyContent: 'space-between',
359 | },
360 | topBar: {
361 | flex: 0.2,
362 | backgroundColor: 'transparent',
363 | flexDirection: 'row',
364 | justifyContent: 'space-around',
365 | paddingTop: Constants.statusBarHeight / 2,
366 | },
367 | bottomBar: {
368 | paddingBottom: isIPhoneX ? 25 : 5,
369 | backgroundColor: 'transparent',
370 | alignSelf: 'flex-end',
371 | justifyContent: 'space-between',
372 | flex: 0.12,
373 | flexDirection: 'row',
374 | },
375 | noPermissions: {
376 | flex: 1,
377 | alignItems:'center',
378 | justifyContent: 'center',
379 | padding: 10,
380 | },
381 | gallery: {
382 | flex: 1,
383 | flexDirection: 'row',
384 | flexWrap: 'wrap',
385 | },
386 | toggleButton: {
387 | flex: 0.25,
388 | height: 40,
389 | marginHorizontal: 2,
390 | marginBottom: 10,
391 | marginTop: 20,
392 | padding: 5,
393 | alignItems: 'center',
394 | justifyContent: 'center',
395 | },
396 | autoFocusLabel: {
397 | fontSize: 20,
398 | fontWeight: 'bold'
399 | },
400 | bottomButton: {
401 | flex: 0.3,
402 | height: 58,
403 | justifyContent: 'center',
404 | alignItems: 'center',
405 | },
406 | newPhotosDot: {
407 | position: 'absolute',
408 | top: 0,
409 | right: -5,
410 | width: 8,
411 | height: 8,
412 | borderRadius: 4,
413 | backgroundColor: '#4630EB'
414 | },
415 | options: {
416 | position: 'absolute',
417 | bottom: 80,
418 | left: 30,
419 | width: 200,
420 | height: 160,
421 | backgroundColor: '#000000BA',
422 | borderRadius: 4,
423 | padding: 10,
424 | },
425 | detectors: {
426 | flex: 0.5,
427 | justifyContent: 'space-around',
428 | alignItems: 'center',
429 | flexDirection: 'row',
430 | },
431 | pictureQualityLabel: {
432 | fontSize: 10,
433 | marginVertical: 3,
434 | color: 'white'
435 | },
436 | pictureSizeContainer: {
437 | flex: 0.5,
438 | alignItems: 'center',
439 | paddingTop: 10,
440 | },
441 | pictureSizeChooser: {
442 | alignItems: 'center',
443 | justifyContent: 'space-between',
444 | flexDirection: 'row'
445 | },
446 | pictureSizeLabel: {
447 | flex: 1,
448 | alignItems: 'center',
449 | justifyContent: 'center'
450 | },
451 | facesContainer: {
452 | position: 'absolute',
453 | bottom: 0,
454 | right: 0,
455 | left: 0,
456 | top: 0,
457 | },
458 | face: {
459 | padding: 10,
460 | borderWidth: 2,
461 | borderRadius: 2,
462 | position: 'absolute',
463 | borderColor: '#FFD700',
464 | justifyContent: 'center',
465 | backgroundColor: 'rgba(0, 0, 0, 0.5)',
466 | },
467 | landmark: {
468 | width: landmarkSize,
469 | height: landmarkSize,
470 | position: 'absolute',
471 | backgroundColor: 'red',
472 | },
473 | faceText: {
474 | color: '#FFD700',
475 | fontWeight: 'bold',
476 | textAlign: 'center',
477 | margin: 10,
478 | backgroundColor: 'transparent',
479 | },
480 | row: {
481 | flexDirection: 'row',
482 | },
483 | });
--------------------------------------------------------------------------------
/src/components/EdgeSlider.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of EdgeSlider Component for croping picture.
3 | *
4 | * This source was adapted from and inspired by Tomas Roos's "React Native Multi Slider."
5 | * @see https://github.com/ptomasroos/react-native-multi-slider/blob/master/converters.js
6 | *
7 | * last modified : 2019.01.09
8 | * @module component/EdgeSlider
9 | * @author Seungho.Yi
10 | * @package react-native-image-kit
11 | * @license MIT
12 | */
13 |
14 | import React from 'react';
15 | import PropTypes from 'prop-types';
16 |
17 | import { Animated, View, ViewPropTypes, StyleSheet } from 'react-native';
18 | import { GestureHandler } from 'expo';
19 | import { createArray, positionToValue, valueToPosition } from "../lib";
20 | const { PanGestureHandler, State } = GestureHandler;
21 |
22 | export default class EdgeSlider extends React.Component {
23 | static propTypes = {
24 | value: PropTypes.number,
25 | style: ViewPropTypes.style,
26 | edgeLength: PropTypes.number,
27 | trackRadius: PropTypes.number,
28 | min: PropTypes.number,
29 | max: PropTypes.number,
30 | sliderLength: PropTypes.number,
31 | step: PropTypes.number,
32 | direction: PropTypes.string,
33 | edgeColor: PropTypes.string,
34 | trackColor: PropTypes.string,
35 | trackColor1: PropTypes.string,
36 | trackColor2: PropTypes.string,
37 |
38 | onValuesChange: PropTypes.func,
39 | };
40 |
41 | static defaultProps = {
42 | value: 0,
43 | edgeLength: 300,
44 | trackRadius: 41,
45 | min: 0,
46 | max: 100,
47 | sliderLength: 100,
48 | step: 1,
49 | direction: 'down',
50 | edgeColor: '#095FFF',
51 | trackColor: null,
52 | trackColor1: 'rgba(0, 0, 0, 0.2)',
53 | trackColor2: 'rgba(255, 255, 255, 0.2)',
54 |
55 | onValuesChange: value => { },
56 | };
57 |
58 | constructor(props) {
59 | super(props);
60 | this.optionsArray = createArray(props.min, props.max, props.step);
61 | let initialPosition = valueToPosition(props.value, this.optionsArray, props.sliderLength);
62 | this.value = props.value;
63 | this._direction = props.direction.toLowerCase();
64 | this._translate = new Animated.Value(0);
65 | if(this._direction === 'up' || this._direction === 'rtl')
66 | this._offset = props.sliderLength - initialPosition;
67 | else
68 | this._offset = initialPosition;
69 | this._translate.setOffset(this._offset);
70 | }
71 |
72 | _onGestureEvent = event => {
73 | const {translationX, translationY} = event.nativeEvent;
74 | const sliderLength = this.props.sliderLength;
75 | let x = this._offset + translationX;
76 | let y = this._offset + translationY;
77 | switch(this._direction) {
78 | case 'down':
79 | case 'up':
80 | if(y >= 0 && y <= sliderLength)
81 | this._translate.setValue(translationY);
82 | break;
83 | case 'ltr':
84 | case 'rtl':
85 | if(x >= 0 && x <= sliderLength)
86 | this._translate.setValue(translationX);
87 | break;
88 | default:
89 | return;
90 | }
91 | };
92 |
93 | _onHandlerStateChange = event => {
94 | const {translationX, translationY} = event.nativeEvent;
95 | const sliderLength = this.props.sliderLength;
96 | let position = 0;
97 | if (event.nativeEvent.oldState === State.ACTIVE) {
98 | switch(this._direction) {
99 | case 'down':
100 | position = this._offset + translationY;
101 | this.value = positionToValue( position, this.optionsArray, this.props.sliderLength );
102 | position = valueToPosition( this.value, this.optionsArray, this.props.sliderLength );
103 | this._offset = position;
104 | break;
105 | case 'up':
106 | position = sliderLength - this._offset - translationY;
107 | this.value = positionToValue( position, this.optionsArray, this.props.sliderLength );
108 | position = valueToPosition( this.value, this.optionsArray, this.props.sliderLength );
109 | this._offset = sliderLength - position;
110 | break;
111 | case 'ltr':
112 | position = this._offset + translationX;
113 | this.value = positionToValue( position, this.optionsArray, this.props.sliderLength );
114 | position = valueToPosition( this.value, this.optionsArray, this.props.sliderLength );
115 | this._offset = position;
116 | break;
117 | case 'rtl':
118 | position = sliderLength - this._offset - translationX;
119 | this.value = positionToValue( position, this.optionsArray, this.props.sliderLength );
120 | position = valueToPosition( this.value, this.optionsArray, this.props.sliderLength );
121 | this._offset = sliderLength - position;
122 | break;
123 | default:
124 | return;
125 | }
126 | this._translate.setOffset(this._offset);
127 | this._translate.setValue(0);
128 | this.props.onValuesChange(this.value);
129 | }
130 | };
131 |
132 | componentWillReceiveProps(nextProps, nextContext) {
133 | const {direction, min, max, value, sliderLength, step} = nextProps;
134 | if(this._direction !== direction){
135 | this._direction = direction;
136 | }
137 | if ( this._direction !== direction.toLowerCase() ||
138 | min !== this.props.min ||
139 | max !== this.props.max ||
140 | value !== this.props.value ||
141 | step !== this.props.step ||
142 | sliderLength !== this.props.sliderLength) {
143 | this.optionsArray = createArray(min, max, step);
144 | let initialPosition = valueToPosition(value, this.optionsArray, sliderLength);
145 | this.value = value;
146 | this._direction = direction.toLowerCase();
147 | this._translate = new Animated.Value(0);
148 | if(this._direction === 'up' || this._direction === 'rtl')
149 | this._offset = sliderLength - initialPosition;
150 | else
151 | this._offset = initialPosition;
152 | this._translate.setOffset(this._offset);
153 | }
154 | }
155 |
156 | render() {
157 | const {style, edgeLength, trackRadius, sliderLength} = this.props;
158 | let styles = this.getStyle(edgeLength, trackRadius, sliderLength, );
159 | return (
160 |
163 |
164 |
165 |
166 |
167 |
168 |
169 | );
170 | }
171 |
172 | getStyle(edgeLength, trackRadius, sliderLength) {
173 | let { trackColor1, trackColor2, trackColor } = this.props;
174 | if (trackColor) {
175 | trackColor1 = trackColor;
176 | trackColor2 = trackColor;
177 | }
178 | let css = {
179 | track : {
180 | display : 'flex',
181 | flexDirection: 'column',
182 | },
183 | track1 : {
184 | flex: 1,
185 | backgroundColor: trackColor1,
186 | },
187 | track2 : {
188 | flex: 1,
189 | backgroundColor: trackColor2,
190 | },
191 | marker : {
192 | position: 'absolute',
193 | backgroundColor: this.props.edgeColor,
194 | }
195 | };
196 |
197 | switch(this._direction){
198 | case 'down':
199 | css.marker.left = -(edgeLength - trackRadius)/2;
200 | css.marker.width = edgeLength;
201 | css.marker.height = 1;
202 | css.marker.transform = [ { translateY: this._translate }, ];
203 | css.track.flexDirection = 'row';
204 | css.track.width = trackRadius;
205 | css.track.height = sliderLength;
206 | css.track1.borderBottomLeftRadius = trackRadius;
207 | css.track2.borderBottomRightRadius = trackRadius;
208 | break;
209 | case 'up':
210 | css.marker.left = -(edgeLength - trackRadius)/2;
211 | css.marker.width = edgeLength;
212 | css.marker.height = 1;
213 | css.marker.transform = [ { translateY: this._translate }, ];
214 | css.track.flexDirection = 'row';
215 | css.track.width = trackRadius;
216 | css.track.height = sliderLength;
217 | css.track1.borderTopLeftRadius = trackRadius;
218 | css.track2.borderTopRightRadius = trackRadius;
219 | break;
220 | case 'ltr':
221 | css.marker.top = -(edgeLength - trackRadius)/2;
222 | css.marker.width = 1;
223 | css.marker.height = edgeLength;
224 | css.marker.transform = [ { translateX: this._translate }, ];
225 | css.track.flexDirection = 'column';
226 | css.track.width = sliderLength;
227 | css.track.height = trackRadius;
228 | css.track1.borderTopRightRadius = trackRadius;
229 | css.track2.borderBottomRightRadius = trackRadius;
230 | break;
231 | case 'rtl':
232 | css.marker.top = -(edgeLength - trackRadius)/2;
233 | css.marker.width = 1;
234 | css.marker.height = edgeLength;
235 | css.marker.transform = [ { translateX: this._translate }, ];
236 | css.track.flexDirection = 'column';
237 | css.track.width = sliderLength;
238 | css.track.height = trackRadius;
239 | css.track1.borderTopLeftRadius = trackRadius;
240 | css.track2.borderBottomLeftRadius = trackRadius;
241 | break;
242 | default:
243 | console.log('EdgeSlider : ', "this.props.direction has improper value." );
244 | return;
245 | }
246 |
247 | return StyleSheet.create(css);
248 | }
249 | }
--------------------------------------------------------------------------------
/src/components/GridContainer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of GridContainer component
3 | * for display pictures as grid style.
4 | * This source was adapted from and inspired by Halil Bilir's "React Native Photo Browser".
5 | * @see https://github.com/halilb/react-native-photo-browser
6 | *
7 | * last modified : 2019.01.28
8 | * @module components/GridContainer
9 | * @author Seungho.Yi
10 | * @package react-native-image-kit
11 | * @license MIT
12 | */
13 |
14 |
15 | import React from 'react';
16 | import PropTypes from 'prop-types';
17 | import { Dimensions, FlatList, TouchableHighlight, View, StyleSheet, ViewPropTypes } from 'react-native';
18 |
19 | import Photo from './Photo';
20 |
21 | // 1 margin and 1 border width
22 | const ITEM_MARGIN = 2;
23 |
24 | export default class GridContainer extends React.Component {
25 |
26 | static propTypes = {
27 | style: ViewPropTypes.style,
28 | pictureList: PropTypes.object.isRequired,
29 | square: PropTypes.bool,
30 | onPhotoTap: PropTypes.func,
31 | itemPerRow: PropTypes.number,
32 |
33 | /*
34 | * refresh the list to apply selection change
35 | */
36 | onMediaSelection: PropTypes.func,
37 |
38 | /**
39 | * offsets the width of the grid
40 | */
41 | offset: PropTypes.number,
42 | };
43 |
44 | static defaultProps = {
45 | style: null,
46 | onPhotoTap: () => {},
47 | itemPerRow: 3,
48 | offset: 0,
49 | square: false,
50 | };
51 |
52 | constructor(props) {
53 | super(props);
54 | this.itemPerRow = props.itemPerRow;
55 | }
56 |
57 | keyExtractor = (item, index) => index.toString();
58 |
59 | renderItem = ({ item, index }) => {
60 | const {
61 | onPhotoTap,
62 | square,
63 | offset,
64 | } = this.props;
65 | const itemPerRow = this.itemPerRow;
66 | const screenWidth = Dimensions.get('window').width - offset;
67 | const photoWidth = (screenWidth / itemPerRow) - (ITEM_MARGIN * (itemPerRow-1));
68 | return (
69 | onPhotoTap(index)}>
70 |
71 |
79 |
80 |
81 | );
82 | };
83 |
84 | render() {
85 | const { pictureList } = this.props;
86 | const itemPerRow = this.itemPerRow;
87 | return (
88 |
89 |
98 |
99 | );
100 | }
101 | }
102 |
103 | const styles = StyleSheet.create({
104 | container: {
105 | flex: 1,
106 | },
107 | row: {
108 | justifyContent: 'center',
109 | margin: 1,
110 | alignItems: 'center',
111 | borderWidth: 1,
112 | borderRadius: 1,
113 | },
114 | });
115 |
--------------------------------------------------------------------------------
/src/components/OverlayMenu.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of OverlayMenu component
3 | * for right side icon bar
4 | * This component was adapted from and inspired by @rt2zz's "React Native Drawer"
5 | * @see https://github.com/root-two/react-native-drawer
6 | *
7 | * last modified : 2019.01.28
8 | * @module components/OverlayMenu
9 | * @author Seungho.Yi
10 | * @package react-native-image-kit
11 | * @license MIT
12 | */
13 |
14 | 'use strict';
15 |
16 | import React from 'react';
17 | import { View, StyleSheet } from 'react-native';
18 | import { Drawer, Button, Icon, Text } from 'native-base';
19 | import PropTypes from 'prop-types';
20 | import Common from '../lib/Common';
21 |
22 | export default class OverlayMenu extends React.Component {
23 | static propTypes = {
24 | buttons: PropTypes.array.isRequired,
25 | styles: PropTypes.object,
26 | open: PropTypes.bool,
27 | children: PropTypes.node,
28 | onOpen: PropTypes.func,
29 | onClose: PropTypes.func,
30 | };
31 |
32 | static defaultProps = {
33 | styles: {
34 | drawer: {
35 | backgroundColor: 'rgba(0,0,0,0.2)',
36 | shadowColor: '#000000',
37 | shadowOpacity: 0.8,
38 | shadowRadius: 3
39 | },
40 | main: {},
41 | drawerOverlay: {},
42 | mainOverlay: {},
43 | },
44 | open: false,
45 | onOpen: null,
46 | onClose: null,
47 | };
48 |
49 | constructor(props) {
50 | super(props);
51 | this.drawer = null;
52 | this.state = { open: props.open };
53 | this.open = this.open.bind(this);
54 | this.close = this.close.bind(this);
55 | this._onOpen = this._onOpen.bind(this);
56 | this._onClose = this._onClose.bind(this);
57 | }
58 |
59 | componentWillReceiveProps(nextProps, nextContext) {
60 | if(this.props.open !== nextProps.open){
61 | this.setState({open: nextProps.open});
62 | }
63 | }
64 |
65 | open() {
66 | this.setState({open: true});
67 | }
68 |
69 | _onOpen() {
70 | if( !this.state.open ) this.setState({open: true});
71 | if(this.props.onOpen) this.props.onOpen();
72 | }
73 |
74 | close() {
75 | this.setState({open: false});
76 | }
77 |
78 | _onClose() {
79 | if( this.state.open ) this.setState({open: false});
80 | if(this.props.onClose) this.props.onClose();
81 | }
82 |
83 | toggle() {
84 | if(this.state.open){
85 | this.close();
86 | }else{
87 | this.open();
88 | }
89 | }
90 |
91 | render() {
92 | const drawerStyles = this.props.styles;
93 | let buttons = this.props.buttons;
94 | let content = (
95 |
96 |
99 | { buttons.map( (v, i) =>{
100 | v.key = i;
101 | v.icon.style = v.icon.style ? [styles.icon, v.icon.style] : styles.icon;
102 | v.style = v.style ? [ styles.button , v.style] : styles.button;
103 | v.transparent = true;
104 | let callback = () => {
105 | this.close();
106 | v.callback(v.arg);
107 | };
108 | return Common.button(v, callback);
109 | } ) }
110 |
111 | );
112 | return (
113 | { this.drawer = ref; }} open={this.state.open}
114 | side={'right'} type={"overlay"} openDrawerOffset={0}
115 | onOpen={this._onOpen} onClose={this._onClose}
116 | styles={drawerStyles}
117 | content={content}
118 | >
119 | {this.props.children}
120 |
121 | );
122 | }
123 | }
124 |
125 | const styles = StyleSheet.create({
126 | button: {
127 | padding: 0,
128 | marginTop: 8,
129 | },
130 | modal: {
131 | display: 'flex',
132 | flexDirection: 'column',
133 | backgroundColor: 'rgba(0,0,0,0.4)',
134 | justifyContent: 'flex-start',
135 | alignItems: 'center',
136 | paddingTop: 20,
137 | },
138 | absolute: {
139 | position: "absolute",
140 | top: 0,
141 | bottom: 0,
142 | right: 0,
143 | width:60,
144 | },
145 | icon: {
146 | color:'#F0F0F0',
147 | fontSize: 28,
148 | },
149 | closeButton: {
150 | borderBottomWidth: 1,
151 | borderBottomColor: '#A0A0A0',
152 | },
153 | });
154 |
--------------------------------------------------------------------------------
/src/components/Photo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of Photo component
3 | * for display and crop picture object
4 | * This source was adapted from and inspired by Halil Bilir's "React Native Photo Browser".
5 | * @see https://github.com/halilb/react-native-photo-browser
6 | *
7 | * last modified : 2019.01.28
8 | * @module components/Photo
9 | * @author Seungho.Yi
10 | * @package react-native-image-kit
11 | * @license MIT
12 | */
13 |
14 | import React, { Component } from 'react';
15 | import PropTypes from 'prop-types';
16 | import { Dimensions, Image, StyleSheet, View, ActivityIndicator, Platform, } from 'react-native';
17 | import { Icon } from 'native-base';
18 |
19 | import * as Progress from 'react-native-progress';
20 | import EdgeSlider from "./EdgeSlider";
21 |
22 |
23 | export default class Photo extends Component {
24 |
25 | static propTypes = {
26 | picture: PropTypes.object.isRequired,
27 | resizeMode: PropTypes.string,
28 | width: PropTypes.number,
29 | height: PropTypes.number,
30 |
31 | /*
32 | * size of selection images are decided based on this
33 | */
34 | thumbnail: PropTypes.bool,
35 |
36 | /*
37 | * image tag generated using require(asset_path)
38 | */
39 | progressImage: PropTypes.number,
40 |
41 | useCircleProgress: PropTypes.bool,
42 | trackWidth : PropTypes.number,
43 | onSliderChange : PropTypes.func,
44 | };
45 |
46 | static defaultProps = {
47 | resizeMode: 'contain',
48 | thumbnail: false,
49 | trackWidth : 41,
50 | onSliderChange: null,
51 | };
52 |
53 | constructor(props) {
54 | super(props);
55 |
56 | this._onProgress = this._onProgress.bind(this);
57 | this._onError = this._onError.bind(this);
58 | this._onLoad = this._onLoad.bind(this);
59 | this._onSliderChange = this._onSliderChange.bind(this);
60 |
61 | this.state = {
62 | picture: props.picture,
63 | width: props.picture.width,
64 | height: props.picture.height,
65 | progress: 0,
66 | error: false,
67 | };
68 | this.getSize();
69 | }
70 |
71 | getSize = () => {
72 | try{
73 | Image.getSize(this.state.picture.uri, (width, height) => {
74 | if(this.state.width === width && this.state.height === height) return;
75 | else{
76 | this.state.picture.width = width;
77 | this.state.picture.height = height;
78 | this.setState({
79 | width: width,
80 | height: height,
81 | });
82 | }
83 | });
84 | } catch (err){
85 | console.log("getSize in Photo.js", err);
86 | }
87 | };
88 |
89 | componentWillReceiveProps(nextProps, nextContext) {
90 | this.setState({
91 | picture: nextProps.picture,
92 | width: nextProps.picture.width,
93 | height: nextProps.picture.height,
94 | progress: 0,
95 | error: false,
96 | }, this.getSize);
97 | }
98 |
99 | _onProgress(event) {
100 | const progress = event.nativeEvent.loaded / event.nativeEvent.total;
101 | if (!this.props.thumbnail && progress !== this.state.progress) {
102 | this.setState({
103 | progress,
104 | });
105 | }
106 | }
107 |
108 | _onError() {
109 | this.setState({
110 | error: true,
111 | progress: 1,
112 | });
113 | }
114 |
115 | _onLoad() {
116 | this.setState({
117 | progress: 1,
118 | });
119 | }
120 |
121 | _onSliderChange( direction, value ) {
122 | this.props.onSliderChange(direction, value);
123 | }
124 |
125 | _renderProgressIndicator() {
126 | const { useCircleProgress } = this.props;
127 | const { progress } = this.state;
128 |
129 | if (progress < 1) {
130 | if (Platform.OS === 'android') {
131 | return ;
132 | }
133 |
134 | const ProgressElement = useCircleProgress ? Progress.Circle : Progress.Bar;
135 | return (
136 |
141 | );
142 | }
143 | return null;
144 | }
145 |
146 | _renderErrorIcon() {
147 | return (
148 |
149 | );
150 | }
151 |
152 | _renderPhoto(sizeStyle) {
153 | const { resizeMode } = this.props;
154 | return (
155 |
163 | );
164 | }
165 |
166 | _renderEdgeSlider(direction, sizeStyle) {
167 | direction = direction.toLowerCase();
168 | const {width, height} = sizeStyle;
169 | const trackWidth = this.props.trackWidth;
170 | const srcWidth= this.state.picture.width;
171 | const srcHeight= this.state.picture.height;
172 |
173 | if( ! srcWidth || ! srcHeight ) return null;
174 |
175 | let dispHeight = height;
176 | let scale = dispHeight / srcHeight;
177 | let dispWidth = scale * srcWidth;
178 | if (dispWidth > width){
179 | dispWidth = width;
180 | scale = dispWidth / srcWidth;
181 | dispHeight = scale * srcHeight;
182 | }
183 |
184 | let style = { position: 'absolute' };
185 | let edgeLength, sliderLength, max;
186 | switch(direction) {
187 | case 'down':
188 | style.top = (height - dispHeight) / 2;
189 | style.left = (width - trackWidth) / 2;
190 | edgeLength = dispWidth;
191 | sliderLength = dispHeight/2-trackWidth;
192 | max = srcHeight/2-trackWidth/scale;
193 | break;
194 | case 'up':
195 | style.top = height/2 + trackWidth;
196 | style.left = (width - trackWidth) / 2;
197 | edgeLength = dispWidth;
198 | sliderLength = dispHeight/2-trackWidth;
199 | max = srcHeight/2-trackWidth/scale;
200 | break;
201 | case 'ltr':
202 | style.top = (height - trackWidth) / 2;
203 | style.left = (width - dispWidth) / 2;
204 | edgeLength = dispHeight;
205 | sliderLength = dispWidth/2-trackWidth;
206 | max = srcWidth/2-trackWidth/scale;
207 | break;
208 | case 'rtl':
209 | style.top = (height - trackWidth) / 2;
210 | style.right = (width - dispWidth) / 2;
211 | edgeLength = dispHeight;
212 | sliderLength = dispWidth/2-trackWidth;
213 | max = srcWidth/2-trackWidth/scale;
214 | break;
215 | default:
216 | return null;
217 | }
218 |
219 | return (
220 | this._onSliderChange(direction, value) }/>
224 | );
225 | }
226 |
227 | render() {
228 | const { width, height } = this.props;
229 | const screen = Dimensions.get('window');
230 | const error = this.state.error;
231 | const sizeStyle = {
232 | width: width || screen.width,
233 | height: height || screen.height,
234 | };
235 | const directions = ['up', 'down', 'ltr', 'rtl'];
236 | return (
237 |
238 | { error ? this._renderErrorIcon() : this._renderProgressIndicator() }
239 | { this._renderPhoto(sizeStyle) }
240 | { this.props.onSliderChange ? directions.map( item => this._renderEdgeSlider(item, sizeStyle) ) : null}
241 |
242 | );
243 | }
244 | }
245 |
246 | const styles = StyleSheet.create({
247 | container: {
248 | alignItems: 'center',
249 | justifyContent: 'center',
250 | },
251 | image: {
252 | position: 'absolute',
253 | top: 0,
254 | left: 0,
255 | right: 0,
256 | },
257 | });
258 |
--------------------------------------------------------------------------------
/src/components/PhotoBrowser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of PhotoBrowser component
3 | * for 1. browsing pictures in predefined folder
4 | * 2. take picture from photo library or website
5 | * This source was adapted from and inspired by Halil Bilir's "React Native Photo Browser".
6 | * @see https://github.com/halilb/react-native-photo-browser
7 | *
8 | * last modified : 2019.01.28
9 | * @module components/PhotoBrowser
10 | * @author Seungho.Yi
11 | * @package react-native-image-kit
12 | * @license MIT
13 | * @todo : 1. The image grid will be reconstructed in accordance with the screen orientation.
14 | */
15 |
16 |
17 | 'use strict';
18 |
19 | import React from 'react';
20 | import {ViewPropTypes, Dimensions, View, Animated, StyleSheet, StatusBar, Keyboard, Modal, Alert} from 'react-native';
21 | import { Root, Container, Header, Title, Button, Icon, Left, Right, Text, Toast, CardItem, Body, Item, Input, Card, Spinner}
22 | from 'native-base';
23 | import PropTypes from 'prop-types';
24 | import Popup from './Popup';
25 | import { ImagePicker } from "expo";
26 | import GridContainer from "./GridContainer";
27 | import PhotoEditor from "./PhotoEditor";
28 | import { PictureList, Common } from '../lib';
29 | import OverlayMenu from './OverlayMenu';
30 | import CameraScreen from './CameraScreen';
31 |
32 | export default class PhotoBrowser extends React.Component {
33 |
34 | static propTypes = {
35 | isModal: PropTypes.bool,
36 | show: PropTypes.bool,
37 | folder: PropTypes.string,
38 | pictureList: PropTypes.object,
39 | style: ViewPropTypes.style,
40 | square: PropTypes.bool,
41 | customBtn: PropTypes.array,
42 | customEditorBtn: PropTypes.array,
43 | onShare: PropTypes.func,
44 | useSpawn: PropTypes.bool,
45 | usePhotoLib: PropTypes.bool,
46 | useCamera: PropTypes.bool,
47 | getFromWeb: PropTypes.bool,
48 | onClose: PropTypes.func,
49 | orientation: PropTypes.string,
50 | };
51 |
52 | static defaultProps = {
53 | style: null,
54 | isModal: true,
55 | show: false,
56 | folder: 'images',
57 | pictureList: null,
58 | square: false,
59 | customBtn: [],
60 | customEditorBtn: [],
61 | onShare: null,
62 | useSpawn: true,
63 | usePhotoLib: true,
64 | useCamera: true,
65 | getFromWeb: true,
66 | onClose: null,
67 | orientation: 'auto',
68 | };
69 |
70 | constructor(props) {
71 | super(props);
72 | const {width, height} = Dimensions.get('window');
73 | this.pictureList = props.pictureList ? props.pictureList : new PictureList(props.folder);
74 | this._customBtn = [];
75 | this._customEditorBtn = [];
76 | this._orientation = [];
77 | this.uriModal = null;
78 | this.cameraModal = null;
79 | this.spinnerModal = null;
80 | this.customMenu = null;
81 | this.imageRequestUri = '';
82 | this.state = {
83 | width : width,
84 | height: height,
85 | headerHeight : Common.header.height,
86 | show : props.show,
87 | gridShow : true,
88 | menuShow : true,
89 | isFullScreen : false,
90 | fullScreenAnim: new Animated.Value(0)
91 | };
92 | this.onHeaderLayout = this.onHeaderLayout.bind(this);
93 | this.closeModal = this.closeModal.bind(this);
94 | this._onGridPhotoTap = this._onGridPhotoTap.bind(this);
95 | this._onCloseEditor = this._onCloseEditor.bind(this);
96 | this._onDelete = this._onDelete.bind(this);
97 | this._onSpawn = this._onSpawn.bind(this);
98 | }
99 |
100 | get customBtn() {
101 | return this.props.customBtn.concat(this._customBtn);
102 | }
103 |
104 | set customBtn(btns) {
105 | this._customBtn = btns;
106 | }
107 |
108 | get customEditorBtn() {
109 | return this.props.customEditorBtn.concat(this._customEditorBtn);
110 | }
111 |
112 | set customEditorBtn(btns) {
113 | this._customEditorBtn = btns;
114 | }
115 |
116 | set supportedOrientations(orientation){
117 | this._orientation = [orientation];
118 | }
119 |
120 | get supportedOrientations(){
121 | let {orientation} = this.props;
122 | if(orientation){
123 | orientation = orientation.toLowerCase();
124 | switch (orientation){
125 | case 'auto' :
126 | return this._orientation;
127 | case 'landscape':
128 | case 'portrait':
129 | return [orientation];
130 | default:
131 | break;
132 | }
133 | }
134 | return ['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right'];
135 | }
136 |
137 | componentWillReceiveProps(nextProps, nextContext) {
138 | if( this.props.show === true && nextProps.show === false ){
139 | this._close();
140 | } else if ( this.props.show === false && nextProps.show === true ){
141 | this._open();
142 | }
143 | }
144 |
145 | onHeaderLayout(e) {
146 | const height = e.nativeEvent.layout.height;
147 | const scr = Dimensions.get('window');
148 | let state = {};
149 | if (this.state.headerHeight !== height ){
150 | state.headerHeight = height;
151 | }
152 | if ( this.state.width !== scr.width || this.state.height !== scr.height){
153 | state.width = scr.width;
154 | state.height = scr.height;
155 | }
156 | if(Object.keys(state).length > 0){
157 | this.setState(state);
158 | }
159 | }
160 |
161 | _onGridPhotoTap(index) {
162 | //console.log('_onGridPhotoTap : ',index);
163 | this.pictureList.currentIndex = index;
164 | this._toggleFullScreen(true);
165 | }
166 |
167 | _onCloseEditor() {
168 | this._toggleFullScreen(false);
169 | }
170 |
171 | async _onSpawn() {
172 | let media = await this.pictureList.spawn();
173 | if(media) this.forceUpdate();
174 | return media;
175 | }
176 |
177 | _getImageFromPhotoLib = async () => {
178 | this.setState({gridShow: false});
179 | let picture = null;
180 | let result = await ImagePicker.launchImageLibraryAsync({
181 | mediaTypes: ImagePicker.MediaTypeOptions.Images,
182 | allowsEditing: true,
183 | });
184 | if ( result.cancelled ) {
185 | this.setState({gridShow: true});
186 | }else{
187 | //console.log('onImageAdd1', result);
188 | let {uri, width, height} = result;
189 | picture = await this.pictureList.insert(uri, width, height);
190 | //console.log('onImageAdd2', picture);
191 | if (picture === null){
192 | Toast.show({
193 | text: "Wrong Image Type (not JPG nor PNG)!",
194 | buttonText: "Okay",
195 | type: "warning"
196 | });
197 | }
198 | this.setState({gridShow: true});
199 | }
200 | return picture;
201 | };
202 |
203 | _getImageByURI = async () => {
204 | this.uriModal.open();
205 | };
206 |
207 | _getImageFromCamera = async () => {
208 | this.cameraModal.open();
209 | };
210 |
211 | _onCancelCamera = async () => {
212 | this.cameraModal.close();
213 | };
214 |
215 | _onTakePhoto = async (photo) => {
216 | let picture = null;
217 | this.cameraModal.close();
218 | this.spinnerModal.open();
219 | console.log(photo);
220 | let {uri, width, height} = photo;
221 | picture = await this.pictureList.insert(uri, width, height);
222 | this.spinnerModal.close();
223 | if (picture === null){
224 | Toast.show({
225 | text: "Wrong Image Type (not JPG nor PNG)!",
226 | buttonText: "Okay",
227 | type: "warning",
228 | duration: 3000,
229 | });
230 | }else{
231 | this.pictureList.currentIndex = 0;
232 | this._toggleFullScreen(true);
233 | }
234 | };
235 |
236 | _requestImageCancel = async () => {
237 | this.uriModal.close();
238 | };
239 |
240 | _requestImage = async () => {
241 | let picture = null;
242 | this.uriModal.close();
243 | this.spinnerModal.open();
244 | if(this.imageRequestUri){
245 | picture = await this.pictureList.insert(this.imageRequestUri);
246 | }
247 | this.spinnerModal.close();
248 | if (picture === null){
249 | Toast.show({
250 | text: "Wrong Image Type (not JPG nor PNG)!",
251 | buttonText: "Okay",
252 | type: "warning",
253 | duration: 3000,
254 | });
255 | }else{
256 | this.pictureList.currentIndex = 0;
257 | this._toggleFullScreen(true);
258 | }
259 | };
260 |
261 | _onDelete( ) {
262 | Alert.alert(
263 | 'Notice',
264 | 'Do you want delete this image and go back to the list?',
265 | [
266 | { text: 'No', onPress: () => console.log('Cancel delete image.'), style: 'cancel'},
267 | {
268 | text: 'Yes',
269 | onPress: async () => {
270 | await this.pictureList.remove();
271 | this._toggleFullScreen(false);
272 | }
273 | },
274 | ]
275 | );
276 | };
277 |
278 | _toggleFullScreen(display) {
279 | this.setState({ isFullScreen: display });
280 | Animated.timing(
281 | this.state.fullScreenAnim,
282 | {
283 | toValue: display ? 1 : 0,
284 | duration: 300,
285 | }
286 | ).start();
287 | if(!display && this.pictureList.length > 0)
288 | this.pictureList.current.reset();
289 | }
290 |
291 | _toggleCustomMenu() {
292 | if(this.customMenu){
293 | this.customMenu.toggle();
294 | }
295 | }
296 |
297 | _onOpenCustomMenu = () => {
298 | this.setState({menuShow: false});
299 | };
300 |
301 | _onCloseCustomMenu = () => {
302 | this.setState({menuShow: true});
303 | };
304 |
305 | get headerBtn() {
306 | let buttons = this.customBtn;
307 |
308 | if (this.props.getFromWeb){
309 | buttons.push({
310 | callback: this._getImageByURI,
311 | icon: {
312 | name: 'download',
313 | type: 'MaterialCommunityIcons',
314 | },
315 | })
316 | }
317 | if (this.props.usePhotoLib){
318 | buttons.push({
319 | callback: this._getImageFromPhotoLib,
320 | icon: {
321 | name: 'film',
322 | type: 'MaterialCommunityIcons',
323 | },
324 | })
325 | }
326 | if (this.props.useCamera){
327 | buttons.push({
328 | callback: this._getImageFromCamera,
329 | icon: {
330 | name: 'camera',
331 | type: 'MaterialCommunityIcons',
332 | },
333 | })
334 | }
335 | return buttons;
336 | }
337 |
338 | _renderSpinner() {
339 | if (this.props.getFromWeb) {
340 | return (
341 | { this.spinnerModal = e; }} style={styles.modal} backgroundColor={'rgba(0, 0, 0, 0.7)'}>
342 |
343 |
344 |
345 |
346 | );
347 | } else return null
348 | }
349 |
350 | _renderURIModal() {
351 | if (this.props.getFromWeb) {
352 | return (
353 | { this.uriModal = e; }} style={styles.modal}>
354 |
355 |
356 | Input image URL
357 |
358 |
359 |
360 | -
361 |
362 | this.imageRequestUri = v}/>
364 |
365 |
366 | {'The URL should include the image file name because the image type is determined by the file name.'}
367 |
368 |
369 |
370 |
371 |
375 |
379 |
380 |
381 |
382 | );
383 | } else return null
384 | }
385 |
386 | _renderCameraModal() {
387 | if (this.props.getFromWeb) {
388 | return (
389 | { this.cameraModal = e; }} >
390 |
391 |
392 | );
393 | } else return null
394 | }
395 |
396 | renderPhotos() {
397 | if(this.state.show && this.state.gridShow){
398 | //console.log('in renderPhotos');
399 | let container;
400 | if (this.pictureList.length > 0) {
401 | if (this.state.isFullScreen) {
402 | container = (
403 |
414 | );
415 | } else {
416 | const itemPerRow = this.state.width > 480 ? Math.round(this.state.width/240.0) : 2;
417 | //console.log('renderPhotos!! : ', this.pictureList.length);
418 | container = (
419 |
428 |
435 |
436 | );
437 | }
438 | }
439 | return (
440 |
441 | {container}
442 |
443 | );
444 | }else{
445 | return (
446 |
447 | );
448 | }
449 | }
450 |
451 | _renderMenuButton(){
452 | if( this.state.menuShow )
453 | return (
454 |
458 | );
459 | else return null
460 | }
461 |
462 | render() {
463 | const { isModal, style } = this.props;
464 | const container = (
465 |
466 | {this.customMenu = ref;}} buttons={this.headerBtn}
467 | onOpen={this._onOpenCustomMenu} onClose={this._onCloseCustomMenu}>
468 |
469 |
470 |
471 |
475 |
476 |
477 |
484 |
485 |
486 | {this._renderMenuButton()}
487 |
488 |
489 |
490 | {this.renderPhotos()}
491 |
492 |
493 | {this._renderURIModal()}
494 | {this._renderSpinner()}
495 | {this._renderCameraModal()}
496 |
497 | );
498 | if (isModal){
499 | return(
500 |
504 | {container}
505 |
506 | );
507 | } else {
508 | return container;
509 | }
510 | }
511 |
512 | _open() {
513 | const {width, height} = Dimensions.get('window');
514 | let {orientation} = this.props;
515 | if(orientation){
516 | orientation = orientation.toLowerCase();
517 | switch (orientation){
518 | case 'auto' :
519 | if (width > height)
520 | this.supportedOrientations = 'landscape';
521 | else
522 | this.supportedOrientations = 'portrait';
523 | break;
524 | case 'landscape':
525 | case 'portrait':
526 | this.supportedOrientations = orientation;
527 | break;
528 | default:
529 | break;
530 | }
531 | }
532 | let state = {
533 | show : true,
534 | gridShow : true,
535 | isFullScreen : false,
536 | fullScreenAnim: new Animated.Value(0),
537 | };
538 | if ( this.state.width !== width || this.state.height !== height){
539 | state.width = width;
540 | state.height = height;
541 | }
542 | this.setState(state);
543 | }
544 |
545 | openModal(){
546 | Keyboard.dismiss();
547 | this._open();
548 | }
549 |
550 | _close(){
551 | if(this.props.onClose){
552 | this.props.onClose(this.pictureList);
553 | } else {
554 | this.pictureList.cleanup();
555 | }
556 | this.setState({
557 | show: false,
558 | currentIndex : 0,
559 | });
560 | }
561 |
562 | closeModal(){
563 | this._close();
564 | }
565 | }
566 |
567 | const styles = StyleSheet.create({
568 | container: {
569 | flex: 1,
570 | backgroundColor: 'black',
571 | },
572 | header: {
573 | height: Common.header.height,
574 | paddingTop: Common.header.padding,
575 | },
576 | modal: {
577 | paddingTop: 20,
578 | },
579 | card: {
580 | height : 245,
581 | width : 300,
582 | },
583 | input: {
584 | width: 240,
585 | color: '#606060',
586 | },
587 | title: {
588 | fontWeight: 'bold',
589 | },
590 | desc: {
591 | marginTop: 8,
592 | fontSize: 10,
593 | color: "#62B1F6",
594 | },
595 | headerButton: {
596 | padding: 0,
597 | margin: 0,
598 | },
599 | button: {
600 | justifyContent: 'center',
601 | width: 130,
602 | },
603 | icon: {
604 | margin: 0,
605 | padding: 0,
606 | },
607 | successText: {
608 | color : "#5cb85c",
609 | },
610 | warningText: {
611 | color : "#f0ad4e",
612 | paddingLeft: 0,
613 | },
614 | block: {
615 | justifyContent: 'space-evenly',
616 | }
617 | });
618 |
--------------------------------------------------------------------------------
/src/components/PhotoEditor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of PhotoEditor component.
3 | * The editing function depends on the expo.ImageManipulator.
4 | *
5 | * This source was adapted from and inspired by Halil Bilir's "React Native Photo Browser".
6 | * @see https://github.com/halilb/react-native-photo-browser
7 | *
8 | * last modified : 2019.01.28
9 | * @module components/PhotoEditor
10 | * @author Seungho.Yi
11 | * @package react-native-image-kit
12 | * @license MIT
13 | */
14 |
15 |
16 | import React from 'react';
17 | import PropTypes from 'prop-types';
18 | import {View, StyleSheet, Text, TouchableWithoutFeedback, ViewPropTypes, Alert, Dimensions}
19 | from 'react-native';
20 | import {Button, Icon, Input, Item, Card, CardItem, Body, Toast} from 'native-base';
21 | import Popup from './Popup';
22 | import BottomBar from './BottomBar';
23 | import Photo from './Photo';
24 |
25 | const TOOLBAR_HEIGHT = 120;
26 |
27 | export default class PhotoEditor extends React.Component {
28 |
29 | static propTypes = {
30 | style: ViewPropTypes.style,
31 | picture: PropTypes.object.isRequired,
32 | onClose: PropTypes.func.isRequired,
33 | useCircleProgress: PropTypes.bool,
34 | useSpawn: PropTypes.bool,
35 | onDelete: PropTypes.func,
36 | onSpawn: PropTypes.func,
37 | onShare: PropTypes.func,
38 | customBtn: PropTypes.array,
39 | topMargin: PropTypes.number,
40 | bottomMargin: PropTypes.number,
41 | };
42 |
43 | static defaultProps = {
44 | style: null,
45 | useSpawn: true,
46 | useCircleProgress: false,
47 | onDelete: null,
48 | onSpawn: null,
49 | onShare: null,
50 | customBtn: [],
51 | topMargin: 0,
52 | bottomMargin: 0,
53 | };
54 |
55 | constructor(props) {
56 | super(props);
57 |
58 | this.screen = Dimensions.get('window');
59 |
60 | this._onClose = this._onClose.bind(this);
61 | this._onResizeByWidth = this._onResizeByWidth.bind(this);
62 | this._onResizeByHeight = this._onResizeByHeight.bind(this);
63 | this._onResize = this._onResize.bind(this);
64 | this._onCancel = this._onCancel.bind(this);
65 | this._onChangeWidthText = this._onChangeWidthText.bind(this);
66 | this._onChangeHeightText = this._onChangeHeightText.bind(this);
67 | this._onClockwise = this._onClockwise.bind(this);
68 | this._onCClockwise = this._onCClockwise.bind(this);
69 | this._onVerticalFlip = this._onVerticalFlip.bind(this);
70 | this._onHorizontalFlip = this._onHorizontalFlip.bind(this);
71 | this._onUndo = this._onUndo.bind(this);
72 | this._onReset = this._onReset.bind(this);
73 | this._onSpawn = this._onSpawn.bind(this);
74 | this._onCrop = this._onCrop.bind(this);
75 | this._onSliderChange = this._onSliderChange.bind(this);
76 |
77 | this.state = {
78 | picture: props.picture,
79 | modal: false,
80 | resize: '',
81 | width: '0',
82 | height: '0',
83 | };
84 | this.cropArea = {
85 | top: 0,
86 | left: 0,
87 | right: 0,
88 | bottom: 0,
89 | };
90 | this._customBtn = [];
91 | }
92 |
93 | get customBtn() {
94 | return this.props.customBtn.concat(this._customBtn);
95 | }
96 |
97 | set customBtn(btns) {
98 | this._customBtn = btns;
99 | }
100 |
101 | componentWillReceiveProps(nextProps, nextContext) {
102 | this.setState({picture: nextProps.picture});
103 | this.cropArea = {
104 | top: 0,
105 | left: 0,
106 | right: 0,
107 | bottom: 0,
108 | };
109 | //console.log('componentWillReceiveProps:', this.inputWidth._root);
110 | }
111 |
112 | _onLayout = (e) => {
113 | this.screen = Dimensions.get('window');
114 | };
115 |
116 | async _onClose(){
117 | await this.state.picture.cleanup();
118 | this.props.onClose(this.state.picture);
119 | }
120 |
121 | _onResizeByWidth(){
122 | this.setState({
123 | modal: true,
124 | resize: 'w',
125 | width: String(this.state.picture.width),
126 | height : String(this.state.picture.height),
127 | });
128 | }
129 |
130 | _onResizeByHeight(){
131 | this.setState({
132 | modal: true,
133 | resize: 'h',
134 | width: String(this.state.picture.width),
135 | height : String(this.state.picture.height),
136 | });
137 | }
138 |
139 | _onChangeWidthText( text ){
140 | if (text){
141 | const width = Number(text);
142 | this.setState({
143 | width: String(width),
144 | height : String(Math.round(this.state.picture.height / this.state.picture.width * width)),
145 | });
146 | } else {
147 | this.setState({
148 | width: '',
149 | height : '0',
150 | });
151 | }
152 | }
153 |
154 | _onChangeHeightText( text ){
155 | //console.log('_onChangeHeightText', text);
156 | if (text){
157 | const height = Number(text);
158 | this.setState({
159 | height: String(height),
160 | width : String(Math.round(this.state.picture.width / this.state.picture.height * height)),
161 | });
162 | } else {
163 | this.setState({
164 | height: '',
165 | width : '0',
166 | });
167 | }
168 | }
169 |
170 | async _onResize(){
171 | let result = await this.state.picture.resize(this.state.width, this.state.height);
172 | this.setState({
173 | modal: false,
174 | });
175 | if (result === null){
176 | Toast.show({
177 | text: "Can not resize image!",
178 | buttonText: "Close",
179 | type: "warning",
180 | duration: 3000
181 | });
182 | }
183 | }
184 |
185 | _onCancel(){
186 | this.setState({
187 | modal: false,
188 | });
189 | }
190 |
191 |
192 | async _onClockwise() {
193 | await this.state.picture.counterClockwise();
194 | this.forceUpdate();
195 | }
196 |
197 | async _onCClockwise() {
198 | await this.state.picture.clockwise();
199 | this.forceUpdate();
200 | }
201 |
202 | async _onVerticalFlip() {
203 | await this.state.picture.verticalFlip();
204 | this.forceUpdate();
205 | }
206 |
207 | async _onHorizontalFlip() {
208 | await this.state.picture.horizontalFlip();
209 | this.forceUpdate();
210 | }
211 |
212 | async _onCrop() {
213 | const { top, left, right, bottom } = this.cropArea;
214 | const picture = this.state.picture;
215 | const originX = left;
216 | const originY = top;
217 | const width = picture.width - left - right;
218 | const height = picture.height - top - bottom;
219 | //console.log(originX, originY, width, height);
220 | await picture.crop(originX, originY, width, height);
221 | this.forceUpdate();
222 | }
223 |
224 | async _onUndo() {
225 | await this.state.picture.undo();
226 | this.forceUpdate();
227 | }
228 |
229 | async _onReset() {
230 | await this.state.picture.reset();
231 | this.forceUpdate();
232 | }
233 |
234 | _onSpawn() {
235 | const picture = this.state.picture;
236 | Alert.alert(
237 | 'Notice',
238 | 'Keep the original image and create a working copy with the current state.',
239 | [
240 | { text: 'Cancel', onPress: () => console.log('Cancel overwriting image.'), style: 'cancel'},
241 | {
242 | text: 'OK',
243 | onPress: async () => {
244 | let spawned;
245 | if (this.props.onSpawn){
246 | spawned = await this.props.onSpawn(picture);
247 | }else spawned = await picture.spawn();
248 | if (spawned)
249 | this.setState({ picture : spawned});
250 | }
251 | },
252 | ]
253 | );
254 | }
255 |
256 | _onSliderChange(direction, value) {
257 | switch(direction) {
258 | case 'down':
259 | this.cropArea.top = value;
260 | break;
261 | case 'up':
262 | this.cropArea.bottom = value;
263 | break;
264 | case 'ltr':
265 | this.cropArea.left = value;
266 | break;
267 | case 'rtl':
268 | this.cropArea.right = value;
269 | break;
270 | default:
271 | return;
272 | }
273 | }
274 |
275 | _renderPhoto(styles, height) {
276 | const { useCircleProgress } = this.props;
277 | const picture = this.state.picture;
278 |
279 | return (
280 |
281 |
282 |
288 |
289 |
290 |
291 | );
292 | }
293 |
294 | _renderModal(styles) {
295 | let icon, callback, desc, value;
296 | switch(this.state.resize){
297 | case 'w' :
298 | icon = "arrow-expand-vertical";
299 | callback = this._onChangeWidthText;
300 | desc = 'height';
301 | value = this.state.width;
302 | break;
303 | case 'h' :
304 | icon = "arrow-expand-horizontal";
305 | callback = this._onChangeHeightText;
306 | desc = 'width';
307 | value = this.state.height;
308 | break;
309 | default:
310 | return;
311 | }
312 | return (
313 |
314 |
315 | Resize
316 |
317 |
318 |
319 | -
320 |
321 | callback(v)}/>
323 |
324 | {`The ${desc} will be adjusted to maintain the aspect ratio.`}
325 |
326 |
327 |
328 |
331 |
334 |
335 |
336 | );
337 |
338 | }
339 |
340 | get topLineBtn(){
341 | let buttons = [];
342 | if (this.props.onClose){
343 | buttons.push({
344 | callback: this._onClose,
345 | bordered: true,
346 | icon: {
347 | name: 'logout-variant',
348 | type: 'MaterialCommunityIcons',
349 | flip: 'horizontal',
350 | },
351 | })
352 | }
353 | if (this.props.useSpawn){
354 | buttons.push({
355 | callback: this._onSpawn,
356 | bordered: true,
357 | icon: {
358 | name: 'add-to-photos',
359 | type: 'MaterialIcons',
360 | },
361 | text: { label: 'Spawn' }
362 | })
363 | }
364 | buttons = buttons.concat(this.customBtn);
365 | if (this.props.onShare){
366 | buttons.push({
367 | callback: this.props.onShare,
368 | bordered: true,
369 | icon: {
370 | name: 'share',
371 | type: 'MaterialIcons',
372 | },
373 | })
374 | }
375 | if (this.props.onDelete){
376 | buttons.push({
377 | callback: this.props.onDelete,
378 | bordered: true,
379 | icon: {
380 | name: 'delete',
381 | type: 'MaterialIcons',
382 | },
383 | })
384 | }
385 | return buttons;
386 | }
387 |
388 | render() {
389 | const styles = this.getStyle();
390 | const picture = this.state.picture;
391 | const {topMargin, bottomMargin, style} = this.props;
392 | const height = this.screen.height - topMargin - bottomMargin;
393 | const orientation = (this.screen.width < 480 || this.screen.height < 480) ? 'portrait' : null;
394 |
395 | return (
396 |
397 | {this._renderPhoto(styles, height)}
398 |
412 |
413 | {this._renderModal(styles)}
414 |
415 |
416 | );
417 | }
418 |
419 | getStyle() {
420 | return StyleSheet.create({
421 | flex: {
422 | flex: 1,
423 | },
424 | modal: {
425 | paddingTop: 20,
426 | },
427 | card: {
428 | height : 245,
429 | width : 260,
430 | },
431 | input: {
432 | width: 200,
433 | color: '#606060',
434 | },
435 | title: {
436 | fontWeight: 'bold',
437 | },
438 | desc: {
439 | marginTop: 8,
440 | fontSize: 10,
441 | color: "#62B1F6",
442 | },
443 | button: {
444 | justifyContent: 'center',
445 | width: 110,
446 | },
447 | successText: {
448 | color : "#5cb85c",
449 | },
450 | warningText: {
451 | color : "#f0ad4e",
452 | },
453 | block: {
454 | justifyContent: 'space-evenly',
455 | }
456 | });
457 | }
458 | }
459 |
--------------------------------------------------------------------------------
/src/components/Popup.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of Popup component
3 | * for various dialog
4 | * last modified : 2019.01.09
5 | * @module components/Popup
6 | * @author Seungho.Yi
7 | * @package react-native-image-kit
8 | * @license MIT
9 | */
10 | 'use strict';
11 |
12 | import React from 'react';
13 | import { Modal, View, ViewPropTypes, StyleSheet, TouchableWithoutFeedback } from 'react-native';
14 | import PropTypes from 'prop-types';
15 |
16 | export default class Popup extends React.Component {
17 |
18 | static propTypes = {
19 | style: ViewPropTypes.style,
20 | visible: PropTypes.bool,
21 | onDismiss: PropTypes.func,
22 | onShow: PropTypes.func,
23 | children: PropTypes.node,
24 | orientation: PropTypes.string,
25 | backgroundColor: PropTypes.string,
26 | };
27 |
28 | static defaultProps = {
29 | style: null,
30 | visible: false,
31 | onDismiss: null,
32 | onShow: null,
33 | orientation: null,
34 | backgroundColor: 'rgba(0, 0, 0, 0.5)',
35 | };
36 |
37 | constructor(props) {
38 | super(props);
39 | this._root = null;
40 | this.state = { visible: props.visible };
41 | this.open = this.open.bind(this);
42 | this.close = this.close.bind(this);
43 | }
44 |
45 | componentWillReceiveProps(nextProps, nextContext) {
46 | if(this.props.visible !== nextProps.visible)
47 | this.setState({ visible: nextProps.visible });
48 | }
49 |
50 | open() {
51 | if(this.props.onShow){
52 | this.props.onShow();
53 | }
54 | this.setState({visible: true});
55 | }
56 |
57 | close() {
58 | this.setState({visible: false});
59 | if(this.props.onDismiss){
60 | this.props.onDismiss();
61 | }
62 | }
63 |
64 | get supportedOrientations() {
65 | switch(this.props.orientation){
66 | case 'portrait':
67 | return ['portrait'];
68 | case 'landscape':
69 | return ['landscape'];
70 | default:
71 | return ['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right'];
72 | }
73 | }
74 |
75 | render() {
76 | const { style, children } = this.props;
77 | return (
78 | { this._root = e; }} onRequestClose={this.close}
79 | supportedOrientations={this.supportedOrientations}
80 | transparent visible={this.state.visible} >
81 |
83 |
84 |
85 |
86 |
87 |
88 | {children}
89 |
90 |
91 |
92 |
93 | );
94 | }
95 |
96 | get styles() {
97 | return StyleSheet.create({
98 | modal: {
99 | flex: 1,
100 | display: 'flex',
101 | flexDirection: 'column',
102 | backgroundColor: this.props.backgroundColor,
103 | justifyContent: 'flex-start',
104 | alignItems: 'center',
105 | },
106 | wrapper: {
107 | backgroundColor: "white"
108 | },
109 | transparent: {
110 | zIndex: 2,
111 | backgroundColor: 'rgba(0,0,0,0)'
112 | },
113 | absolute: {
114 | position: "absolute",
115 | top: 0,
116 | bottom: 0,
117 | left: 0,
118 | right: 0
119 | }
120 | });
121 | }
122 | };
123 |
--------------------------------------------------------------------------------
/src/lib/Common.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { Platform, Text, Dimensions } from "react-native";
4 | import { Button, Icon } from "native-base";
5 | import React from "react";
6 |
7 | const scr = Dimensions.get("window");
8 | const platform = Platform.OS;
9 | const isIphoneX = platform === "ios" && (scr.height === 812 || scr.width === 812);
10 | const isIphoneXR = platform === "ios" && (scr.height === 896 || scr.width === 896);
11 |
12 | export default {
13 | /* this trim, ltrim, rtrim is copy from https://www.somacon.com/p355.php, © Shailesh N. Humbad */
14 | trim : function(stringToTrim) {
15 | return stringToTrim.replace(/^\s+|\s+$/g,"");
16 | },
17 |
18 | ltrim : function(stringToTrim) {
19 | return stringToTrim.replace(/^\s+/,"");
20 | },
21 |
22 | rtrim : function(stringToTrim) {
23 | return stringToTrim.replace(/\s+$/,"");
24 | },
25 |
26 | /* -------------------------------------------------------------------------------------------- */
27 |
28 | sleep : async function(ms) {
29 | return new Promise(res => setTimeout(res, ms));
30 | },
31 |
32 | equal : function (x, y) {
33 | if ( x === y ) return true;
34 | if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false;
35 | if ( x.constructor !== y.constructor ) return false;
36 | return JSON.stringify(x) === JSON.stringify(y);
37 | },
38 |
39 | defaultIconType : "MaterialCommunityIcons",
40 |
41 | icon : function(obj) {
42 | const type = obj.type ? obj.type : this.defaultIconType;
43 | let styles = [];
44 | if (obj.style) styles.push(obj.style);
45 | if (obj.rotate) {
46 | if ( Number.isInteger(obj.rotate)) obj.rotate += 'deg';
47 | styles.push({transform: [{ rotateZ: obj.rotate }]});
48 | }
49 | if (obj.flip) styles.push(
50 | obj.flip === 'vertical' ? {transform: [{ rotateX: '180deg'}]} : {transform: [{ rotateY: '180deg'}]}
51 | );
52 | if(obj.name)
53 | return (
54 |
55 | );
56 | else return null;
57 | },
58 |
59 | text : function(obj){
60 | let styles = [];
61 | if (obj.style) styles.push(obj.style);
62 | if(obj.label)
63 | return(
64 | {obj.label}
65 | );
66 | else return null;
67 | },
68 |
69 | button : function(obj, callback=null) {
70 | let styles = [];
71 | if (obj.style) styles.push(obj.style);
72 | return (
73 |
96 | );
97 | },
98 |
99 | buttonList : function(list, styles={}, arg=null) {
100 | return list.map( (v, i) => {
101 | v.key = i;
102 | if(arg) v.arg = arg;
103 | if(v.icon && styles.icon)
104 | v.icon.style = v.icon.style ? Object.assign(v.icon.style, styles.icon) : styles.icon;
105 | if(v.text && styles.text)
106 | v.text.style = v.text.style ? Object.assign(v.text.style, styles.text) : styles.text;
107 | v.style = v.style ? Object.assign(v.style, styles.text) : styles.button;
108 | return Common.button(v);
109 | });
110 | },
111 |
112 | platform,
113 |
114 | isIphoneX,
115 | isIphoneXR,
116 |
117 | /* Below is copy from Native-Base */
118 | color: {
119 | primary: platform === "ios" ? "#007aff" : "#3F51B5",
120 | info: "#62B1F6",
121 | success: "#5cb85c",
122 | danger: "#d9534f",
123 | warning: "#f0ad4e",
124 | dark: "#000",
125 | light: "#f4f4f4",
126 | textColor: "#303030",
127 | },
128 |
129 | // Header
130 | header: {
131 | height: isIphoneXR ? 72 : 64,
132 | padding: platform === "ios" ? (isIphoneXR ? 26 : 4) : 18,
133 | },
134 |
135 | statusBar: {
136 | height: platform === "ios" ? 0 : 20,
137 | },
138 |
139 | toolbar: {
140 | btnColor: platform === "ios" ? "#007aff" : "#fff",
141 | defaultBg: platform === "ios" ? "#F8F8F8" : "#3F51B5",
142 | searchIconSize: platform === "ios" ? 20 : 23,
143 | inputColor: platform === "ios" ? "#CECDD2" : "#fff",
144 | btnTextColor: platform === "ios" ? "#007aff" : "#fff",
145 | defaultBorder: platform === "ios" ? "#a7a6ab" : "#3F51B5",
146 | },
147 |
148 | searchBar : {
149 | height: platform === "ios" ? 30 : 40,
150 | inputHeight: platform === "ios" ? 30 : 50,
151 | },
152 |
153 | input: {
154 | fontSize: 17,
155 | borderColor: "#D9D5DC",
156 | successBorderColor: "#2b8339",
157 | errorBorderColor: "#ed2f2f",
158 | heightBase: 50,
159 | },
160 | };
161 |
--------------------------------------------------------------------------------
/src/lib/FileUtil.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of FileUtil Class
3 | * for handling user file in user document directory
4 | * last modified : 2019.01.09
5 | * @module lib/FileUtil
6 | * @author Seungho.Yi
7 | * @package react-native-image-kit
8 | * @license MIT
9 | */
10 | 'use strict';
11 |
12 | import { FileSystem } from "expo";
13 |
14 | const uuidv4 = require('uuid/v4');
15 |
16 |
17 | export default class FileUtil {
18 |
19 | static documentFolder = FileSystem.documentDirectory;
20 | static intermediates = true;
21 | static idempotent = true;
22 |
23 | static fileName(uri) {
24 | let fileName = uri.trim().split('/').pop();
25 | return fileName.split('?').shift();
26 | }
27 |
28 | static path(uri) {
29 | if (uri){
30 | let path = uri.trim().split('/');
31 | if (path.length > 1){
32 | path.pop();
33 | return path.join('/');
34 | }
35 | }
36 | return '';
37 | }
38 |
39 | static ext(uri) {
40 | let buf = FileUtil.fileName(uri).split('.');
41 | if(buf.length > 1)
42 | return buf.pop().toLowerCase();
43 | else return '';
44 | }
45 |
46 | static pureFileName(uri){
47 | let fileName = FileUtil.fileName(uri);
48 | let buf = fileName.split('.');
49 | if(buf.length > 1) {
50 | buf = buf.pop();
51 | return buf.join('.');
52 | }else return fileName;
53 | }
54 |
55 | static isFullPath(uri) {
56 | if (uri){
57 | let path = uri.trim().split('/');
58 | if(path.length > 1)
59 | return path[0].indexOf(':') > 0;
60 | }
61 | return false;
62 | }
63 |
64 | static isFileNameOnly(uri) {
65 | if (uri){
66 | uri = uri.trim();
67 | return uri.indexOf('/', 1) < 0;
68 | }
69 | return false;
70 | }
71 |
72 | static uniqueName(){
73 | return uuidv4();
74 | }
75 |
76 | static async exists(uri) {
77 | try{
78 | const result = await FileSystem.getInfoAsync(uri);
79 | return result.exists;
80 | }catch(err){
81 | console.log("exists in FileUtil.js", err);
82 | }
83 | return false;
84 | }
85 |
86 | static async isFolder(uri) {
87 | try{
88 | const result = await FileSystem.getInfoAsync(uri);
89 | return result.isDirectory ;
90 | }catch(err){
91 | console.log("exists in FileUtil.js", err);
92 | }
93 | return false;
94 | }
95 |
96 | static async copy(src, target){
97 | try{
98 | const options = {
99 | from : src,
100 | to : target,
101 | };
102 | await FileSystem.copyAsync(options);
103 | return true;
104 | } catch(err){
105 | console.log("copy in FileUtil.js : ", err);
106 | return false;
107 | }
108 | }
109 |
110 | static async move(src, target){
111 | try{
112 | let options ={
113 | from : src,
114 | to : target,
115 | };
116 | await FileSystem.moveAsync(options);
117 | return true;
118 | } catch(err){
119 | console.log("move in FileUtil.js", err);
120 | return false;
121 | }
122 | }
123 |
124 | static async delete(src){
125 | try{
126 | let options ={
127 | idempotent : FileUtil.idempotent,
128 | };
129 | await FileSystem.deleteAsync(src, options);
130 | return true;
131 | } catch(err){
132 | console.log("delete in FileUtil.js", err);
133 | return false;
134 | }
135 | }
136 |
137 | static async clearFolder(uri){
138 | try{
139 | const result = await FileSystem.getInfoAsync(uri);
140 | if (result.exists){
141 | if(result.isDirectory){
142 | let result = await FileUtil.fileList(uri);
143 | if (result !== null){
144 | let res = true;
145 | result.forEach( async item => {
146 | let r = await FileUtil.delete(uri + '/' + item);
147 | if ( ! r ) console.log("clearFolder in FileUtil.js : ", item + ' can not delete.');
148 | res = res && r;
149 | });
150 | return res;
151 | }
152 | }else{
153 | console.log("clearFolder in FileUtil.js : ", uri + ' is not a folder.');
154 | }
155 | } else {
156 | console.log("clearFolder in FileUtil.js : ", uri + ' is not exists.');
157 | }
158 | }catch(err){
159 | console.log("clearFolder in FileUtil.js : ", err);
160 | }
161 | return false;
162 | }
163 |
164 | static async makeFolder(src){
165 | try{
166 | let options ={
167 | intermediates : FileUtil.intermediates,
168 | };
169 | await FileSystem.makeDirectoryAsync(src, options);
170 | return true;
171 | } catch(err){
172 | console.log("makeFolder in FileUtil.js", err);
173 | return false;
174 | }
175 | }
176 |
177 | static async confirmFolderExists(uri){
178 | try{
179 | let result = await FileSystem.getInfoAsync(uri);
180 | if(result.exists){
181 | if(result.isDirectory) return true;
182 | else{
183 | console.log(uri + ' is exists but normal file!');
184 | return false;
185 | }
186 | }else{
187 | await FileUtil.makeFolder(uri);
188 | return await FileUtil.exists(uri);
189 | }
190 | }catch(err){
191 | console.log("confirmFolderExists in FileUtil.js", err);
192 | }
193 | return false;
194 | }
195 |
196 | static async fileList(folder){
197 | try{
198 | return await FileSystem.readDirectoryAsync(folder);
199 | } catch(err){
200 | console.log("fileList in FileUtil.js", err);
201 | return null;
202 | }
203 | }
204 |
205 | static async download(src, target){
206 | try{
207 | let options ={
208 | md5 : false,
209 | };
210 | const result = await FileSystem.downloadAsync(src, target, options);
211 | return result.status;
212 | } catch(err){
213 | console.log("download in FileUtil.js", err);
214 | return 0;
215 | }
216 | }
217 |
218 | static async read(uri){
219 | try{
220 | return await FileSystem.readAsStringAsync(uri);
221 | } catch(err) {
222 | console.log("read in FileUtil.js", err);
223 | }
224 | return null;
225 | }
226 |
227 | static async write(uri, data){
228 | try{
229 | await FileSystem.writeAsStringAsync(uri, data);
230 | } catch(err) {
231 | console.log("write in FileUtil.js", err);
232 | }
233 | return null;
234 | }
235 |
236 | static async readJSON(uri){
237 | try{
238 | const json = await FileUtil.read(uri);
239 | if (json) return JSON.parse(json);
240 | } catch(err) {
241 | console.log("readJSON in FileUtil.js", err);
242 | }
243 | return null;
244 | }
245 |
246 | static async writeJSON(uri, obj){
247 | try{
248 | await FileUtil.write(uri, JSON.stringify(obj));
249 | } catch(err) {
250 | console.log("writeJSON in FileUtil.js", err);
251 | }
252 | }
253 | }
--------------------------------------------------------------------------------
/src/lib/Picture.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of Picture Class
3 | * for handling picture file in user document directory
4 | * last modified : 2019.01.09
5 | * @module lib/Picture
6 | * @author Seungho.Yi
7 | * @package react-native-image-kit
8 | * @license MIT
9 | */
10 | 'use strict';
11 |
12 | import { Image } from "react-native";
13 | import { ImageManipulator } from "expo";
14 | import FileUtil from "./FileUtil";
15 |
16 | // Only image files of jpg type and png type are supported.
17 | const imageType = new Set(['jpeg', 'jpg', 'png']);
18 |
19 | let _tempFolderName = '_temp_';
20 |
21 | export default class Picture {
22 |
23 | static get tempFolderName() {
24 | return _tempFolderName;
25 | }
26 |
27 | static set tempFolderName( folder ) {
28 | return _tempFolderName = folder;
29 | }
30 |
31 |
32 | /**
33 | * Ceate a picture
34 | * @param {string} uri - Picture file uri started with 'file:///'.
35 | * @param {number} width - Picture image width. 0 if unknown.
36 | * @param {number} height - Picture image height. 0 if unknown.
37 | * @param {boolean} tempFolderExist - Whether a temporary folder is created to keep the ImageManipulator application results.
38 | */
39 | constructor(uri, width=0, height=0, tempFolderExist = false) {
40 | if(uri.startsWith('file:///') && Picture.availableType(uri)){
41 | this._uri = uri;
42 | this._history = [ ];
43 | this._tempFolder = this.tempFolderURI;
44 | this.selected = false;
45 | this._tempFolderExist = tempFolderExist;
46 | this._initHistory(uri, width, height).catch(e => {
47 | console.log("constructor in Picture.js", e);
48 | });
49 | } else throw "Bad uri. Image file must be local file and png or jpg type.";
50 | }
51 |
52 | get tempFolderURI(){
53 | if (this.hasOwnProperty('_tempFolder')) return this._tempFolder;
54 | else if (this._uri.startsWith(FileUtil.documentFolder))
55 | return FileUtil.path(this._uri) + '/' + Picture.tempFolderName;
56 | else
57 | return FileUtil.documentFolder + '/' + Picture.tempFolderName;
58 | }
59 |
60 | async _initHistory(uri, width=0, height=0){
61 | const target = this.tempFolderURI + '/' + FileUtil.uniqueName() + '.' + this.ext;
62 | let item = {uri: target, width, height};
63 | this._history.unshift(item);
64 | if (! this._tempFolderExist){
65 | this._tempFolderExist = await FileUtil.confirmFolderExists(this._tempFolder);
66 | if (! this._tempFolderExist) return Promise.reject( "Can not create temp folder!");
67 | }
68 | if( await FileUtil.copy(uri, target)){
69 | if ( width === 0 || height === 0 ) await this.calcSize();
70 | } else return Promise.reject( "Can not create history!");
71 | }
72 |
73 | async _unshiftHistory(uri, width=0, height=0){
74 | const target = this.tempFolderURI + '/' + FileUtil.uniqueName() + '.' + this.ext;
75 | if( await FileUtil.move(uri, target) ){
76 | let item = {uri: target, width, height};
77 | this._history.unshift(item);
78 | if ( width === 0 || height === 0 ) await this.calcSize();
79 | return item;
80 | }
81 | return null;
82 | }
83 |
84 | get length() {
85 | return this._history.length;
86 | }
87 |
88 | get width() {
89 | if(this.length === 0) return 0;
90 | return this._history[0].width;
91 | }
92 |
93 | set width( value ) {
94 | if(this.length === 0) return 0;
95 | else return this._history[0].width = value;
96 | }
97 |
98 | get height() {
99 | if(this.length === 0) return 0;
100 | return this._history[0].height;
101 | }
102 |
103 | set height( value ) {
104 | if(this.length === 0) return 0;
105 | else return this._history[0].height = value;
106 | }
107 |
108 | get uri() {
109 | if (this.length > 0) return this._history[0].uri;
110 | else return this._uri;
111 | }
112 |
113 | get source(){
114 | return this._uri;
115 | }
116 |
117 | get path() {
118 | return FileUtil.path(this._uri);
119 | }
120 |
121 | get fileName() {
122 | return FileUtil.fileName(this._uri);
123 | }
124 |
125 | get ext() {
126 | return FileUtil.ext(this.uri);
127 | }
128 |
129 | static availableType(fileName) {
130 | try{
131 | let ext = FileUtil.ext(fileName);
132 | return imageType.has(ext);
133 | } catch (e) {
134 | console.log("availableType in Pictues.js", e);
135 | }
136 | return false;
137 | }
138 |
139 | static type(fileName) {
140 | let ext = FileUtil.ext(fileName);
141 | if (imageType.has(ext)){
142 | if (ext === 'png') return 'png';
143 | else return 'jpeg';
144 | }
145 | return '';
146 | }
147 |
148 | get format() {
149 | return Picture.type(this.uri);
150 | }
151 |
152 | calcSize() {
153 | try{
154 | Image.getSize(this.uri, (width, height) => {
155 | this._history[0].width = width;
156 | this._history[0].height = height;
157 | //console.log("calcSize in Pictues.js", width, height);
158 | });
159 | } catch (err){
160 | console.log("calcSize in Pictues.js", err);
161 | }
162 | }
163 |
164 | async exists() {
165 | return await FileUtil.exists(this.uri);
166 | }
167 |
168 | async _unshift(uri, width=0, height=0){
169 | const item = await this._unshiftHistory(uri, width, height);
170 | if(item) {
171 | if(! await FileUtil.copy(this.uri, this._uri)){
172 | this._history.shift();
173 | console.log('In _unshift of picture.js : ','Can not update original image!');
174 | }
175 | }
176 | return null;
177 | }
178 |
179 | cleanup(){
180 | if ( this.length > 1 ) {
181 | let history = this._history;
182 | this._history = [ history.shift() ];
183 | for (let item of history) {
184 | FileUtil.delete(item.uri);
185 | }
186 | }
187 | return false;
188 | }
189 |
190 | async reset() {
191 | let flag = true;
192 | if ( this.length > 1 ) {
193 | let src = this._history.pop();
194 | if (await FileUtil.copy(src.uri, this._uri)){
195 | flag = src.width === this.width && src.height === this.height;
196 | let history = this._history;
197 | this._history = [src];
198 | for (let item of history) {
199 | await FileUtil.delete(item.uri);
200 | }
201 | } else {
202 | this._history.push(src);
203 | return null;
204 | }
205 | }
206 | return flag;
207 | }
208 |
209 | async undo() {
210 | let flag = true;
211 | if ( this.length > 1 ) {
212 | const last = this._history.shift();
213 | if (await FileUtil.copy(this.uri, this._uri)){
214 | flag = last.width === this.width && last.height === this.height;
215 | await FileUtil.delete(last.uri);
216 | }else{
217 | this._history.unshift(last);
218 | return null;
219 | }
220 | }
221 | return flag;
222 | }
223 |
224 | async remove() {
225 | for (let item of this._history) {
226 | await FileUtil.delete(item.uri);
227 | }
228 | this._history = [ ];
229 | await FileUtil.delete(this.uri);
230 | return this;
231 | }
232 |
233 | async copy(tempFolderExist=false) {
234 | const newPath = this.path + '/' + FileUtil.uniqueName() + '.' + this.ext;
235 | if (await FileUtil.copy(this.uri, newPath)){
236 | return new Picture(newPath, this.width, this.height, tempFolderExist);
237 | }
238 | return null;
239 | }
240 |
241 | async spawn(tempFolderExist=false) {
242 | let picture = await this.copy(tempFolderExist);
243 | if(picture) await this.reset();
244 | return picture;
245 | }
246 |
247 | /**
248 | * Download and create a picture and return.
249 | * @param {string} uri - Picture file uri started with 'file:///' or 'http://'
250 | * @param {number} width - Picture image width. 0 if unknown.
251 | * @param {number} height - Picture image height. 0 if unknown.
252 | * @param {number} downURI - Where picture file downloaded from "uri" will be stored.
253 | * @param {boolean} tempFolderExist
254 | * @returns {Promise}
255 | */
256 | static async getPicture(uri, width=0, height=0, downURI=null, tempFolderExist=false){
257 | const orgName = FileUtil.fileName(uri);
258 | if (! downURI){
259 | downURI = orgName;
260 | }
261 | if (Picture.availableType(downURI)){
262 | const target = FileUtil.isFullPath(downURI) ? downURI : FileUtil.documentFolder + '/' + downURI;
263 | uri.startsWith('file:') ? await FileUtil.move(uri, target) : await FileUtil.download(uri, target);
264 | return new Picture(target, width, height, tempFolderExist);
265 | }
266 | return null;
267 | }
268 |
269 | static compress = 0.8;
270 |
271 | static base64 = false;
272 |
273 | async manipulate(options){
274 | const format = this.format;
275 | if ( format ){
276 | try{
277 | const result = await ImageManipulator.manipulateAsync(this.uri, options,
278 | { compress: Picture.compress, format: format, base64: Picture.base64 });
279 | const flag = this.width === result.width && this.height === result.height;
280 | await this._unshift( result.uri, result.width, result.height );
281 | return flag;
282 | } catch (e) {
283 | console.log("manipulate in Picture :", e, "options :", options);
284 | }
285 | }
286 | return null;
287 | }
288 |
289 | async resize(width, height){
290 | width = Math.round(parseFloat(width));
291 | height = Math.round(parseFloat(height));
292 | if (isNaN(width) || isNaN(height) || width === 0 || height === 0) return null;
293 | return await this.manipulate([{ resize: { width: width, height: height } }]);
294 | }
295 |
296 | async rotate(angle){
297 | angle = parseFloat(angle);
298 | if (isNaN(angle) || angle === 0 ) return null;
299 | return await this.manipulate([{ rotate: angle}]);
300 | }
301 |
302 | async clockwise(){
303 | return await this.rotate( 90 );
304 | }
305 |
306 | async counterClockwise(){
307 | return await this.rotate( 270 );
308 | }
309 |
310 | async crop(originX, originY, width, height){
311 | originX = Math.round(parseFloat(originX));
312 | originY = Math.round(parseFloat(originY));
313 | width = Math.round(parseFloat(width));
314 | height = Math.round(parseFloat(height));
315 | if (isNaN(originX) || isNaN(originY) || isNaN(width) || isNaN(height) || width === 0 || height === 0) return null;
316 | return await this.manipulate([{ crop: { originX: originX, originY: originY, width: width, height: height } }]);
317 | }
318 |
319 | async verticalFlip(){
320 | return await this.manipulate([{ flip: { vertical: true }}]);
321 | }
322 |
323 | async horizontalFlip(){
324 | return await this.manipulate([{ flip: { horizontal: true }}]);
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/src/lib/PictureList.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @overview Definition of PictureList Class
3 | * for handling picture files in specific user folder.
4 | * last modified : 2019.01.09
5 | * @module lib/PictureList
6 | * @author Seungho.Yi
7 | * @package react-native-image-kit
8 | * @license MIT
9 | */
10 |
11 | 'use strict';
12 | import Picture from './Picture';
13 | import FileUtil from './FileUtil';
14 |
15 |
16 | const defaultFolderName = 'images';
17 |
18 | export default class PictureList {
19 |
20 | static indexFile = 'list.json';
21 |
22 | constructor(folder='images'){
23 | this._folder = folder;
24 | this._list = [];
25 | this._currentIndex = 0;
26 | this._initFolder();
27 | }
28 |
29 | get folder() {
30 | return FileUtil.documentFolder + this._folder;
31 | }
32 |
33 | get tempFolder() {
34 | return this.folder + '/' + Picture.tempFolderName;
35 | }
36 |
37 | get indexUri() {
38 | return this.folder + '/' + PictureList.indexFile;
39 | }
40 |
41 | get length() {
42 | return this._list.length;
43 | }
44 |
45 | get(i) {
46 | if (Number.isInteger(i)){
47 | let idx = parseInt(i);
48 | if (idx < 0 || idx >= this.length) return null;
49 | return this._list[idx];
50 | }else {
51 | let item = this.findByURI(i);
52 | if (item === undefined){
53 | let fileName = i.split('/').pop();
54 | item = this.findByName(fileName);
55 | if (item === undefined) return null;
56 | else return item;
57 | }
58 | }
59 | }
60 |
61 | map(func) {
62 | return this._list.map(func);
63 | }
64 |
65 | get list() {
66 | return this._list;
67 | }
68 |
69 | get currentIndex() {
70 | if (this.length === 0) return -1;
71 | else return this._currentIndex;
72 | }
73 |
74 | set currentIndex(idx) {
75 | if (idx < 0 || idx >= this.length) return -1;
76 | else return this._currentIndex = idx;
77 | }
78 |
79 | next() {
80 | if (this.length === 0) return null;
81 | let idx = this.currentIndex + 1;
82 | if (idx >= this.length) idx = 0;
83 | this.currentIndex = idx;
84 | return this.current();
85 | }
86 |
87 | prev() {
88 | if (this.length === 0) return null;
89 | let idx = this.currentIndex - 1;
90 | if (idx < 0) idx = this.length - 1;
91 | this.currentIndex = idx;
92 | return this.current();
93 | }
94 |
95 | get current() {
96 | if (this.length === 0) return null;
97 | else return this._list[this._currentIndex];
98 | }
99 |
100 | find(filter) {
101 | return this._list.find(filter);
102 | }
103 |
104 | findIndex(filter) {
105 | return this._list.findIndex(filter);
106 | }
107 |
108 | findByName(fileName) {
109 | return this.find( item => item.fileName === fileName);
110 | }
111 |
112 | findIndexByName(fileName) {
113 | return this.findIndex( item => item.fileName === fileName);
114 | }
115 |
116 | findByURI(uri) {
117 | return this.find( item => item.uri === uri);
118 | }
119 |
120 | findIndexByURI(uri) {
121 | return this.findIndex( item => item.uri === uri);
122 | }
123 |
124 | async _initFolder(){
125 | if (await FileUtil.confirmFolderExists(this.folder)){
126 | if (await FileUtil.confirmFolderExists(this.tempFolder)){
127 | await FileUtil.clearFolder(this.tempFolder);
128 | if(await FileUtil.exists(this.indexUri)){
129 | if (! await this._readMediaList())
130 | await this._rebuildMediaList();
131 | }else{
132 | await this._rebuildMediaList();
133 | }
134 | } else throw "_initFolder of PictureList : Can not use folder " + this.tempFolder;
135 | } else throw "_initFolder of PictureList : Can not use folder " + this.folder;
136 | }
137 |
138 | async clearTempFolder(){
139 | await FileUtil.clearFolder(this.tempFolder);
140 | }
141 |
142 | async writeMediaList(){
143 | const list = this.map( item => {
144 | return { fileName: item.fileName, width: item.width, height: item.height };
145 | });
146 | await FileUtil.writeJSON(this.indexUri, list);
147 | }
148 |
149 | async _readMediaList(){
150 | let list = await FileUtil.readJSON(this.indexUri);
151 | if (list === null) list = [];
152 | let check = true;
153 | for ( let item of list ){
154 | let r = await FileUtil.exists(this.folder + '/' + item.fileName) && Picture.availableType(item.fileName);
155 | check = check && r;
156 | }
157 | if (check){
158 | this._list = list.map( item => {
159 | if (item.width && item.height)
160 | return new Picture(this.folder + '/' + item.fileName, item.width, item.height, true);
161 | else
162 | return new Picture(this.folder + '/' + item.fileName, 0, 0, true);
163 | });
164 | }
165 | return check;
166 | }
167 |
168 | async _rebuildMediaList() {
169 | this._list = [];
170 | let result = await FileUtil.fileList(this.folder);
171 | if (result !== null){
172 | result.forEach( item => {
173 | let fileName = FileUtil.fileName(item);
174 | if (Picture.availableType(fileName))
175 | this._list.push(new Picture(this.folder + '/' + fileName, 0, 0, true));
176 | });
177 | }
178 | await this.writeMediaList();
179 | }
180 |
181 | async check() {
182 | let ghost = [];
183 | for (let item of this._list){
184 | let r = await item.exists();
185 | if(r){
186 | item.calcSize();
187 | }else{
188 | ghost.push(item);
189 | }
190 | console.log('check : ', item.fileName, r);
191 | }
192 | if(ghost.length > 0){
193 | while(ghost.length > 0){
194 | await this.remove(ghost.pop().uri)
195 | }
196 | }
197 | }
198 |
199 | async reset() {
200 | let r = true;
201 | for (let item of this._list){
202 | if ( null === await item.reset() )
203 | r = false;
204 | }
205 | await this.writeMediaList();
206 | return r;
207 | }
208 |
209 | async cleanup() {
210 | let r = true;
211 | for (let item of this._list){
212 | if ( null === await item.cleanup())
213 | r = false;
214 | }
215 | await this.writeMediaList();
216 | return r;
217 | }
218 |
219 | getIndex(uri=null){
220 | if (uri===null) return this.currentIndex;
221 | let idx;
222 | if (Number.isInteger(uri)){
223 | idx = parseInt(uri);
224 | }else{
225 | let fileName = FileUtil.fileName(uri);
226 | idx = this.findIndexByName(fileName);
227 | if (idx < 0){
228 | idx = this.findIndexByURI(uri);
229 | }
230 | }
231 | if (idx < this.length) return idx;
232 | else return -1;
233 | }
234 |
235 | async insert(src, width=0, height=0){
236 | if (Picture.availableType(src)) {
237 | const target = this.folder + '/' + FileUtil.uniqueName() + '.' + FileUtil.ext(src);
238 | let picture = await Picture.getPicture(src, width, height, target, true);
239 | if (picture !== null) {
240 | this._list.unshift(picture);
241 | await this.writeMediaList();
242 | return picture;
243 | }
244 | }
245 | return null;
246 | }
247 |
248 | async spawn(){
249 | let picture = await this.current.spawn(true);
250 | if (picture !== null){
251 | this._list.unshift(picture);
252 | this.currentIndex = 0;
253 | await this.writeMediaList();
254 | } else {
255 | console.log('PictureList.spawn : ', picture);
256 | }
257 | return picture;
258 | }
259 |
260 | async remove(uri=null){
261 | let idx = this.getIndex(uri);
262 | if (idx < 0) return null;
263 | try{
264 | let picture = this.get(idx);
265 | if (await picture.remove()){
266 | if (idx === this.currentIndex && idx > 0)
267 | this.currentIndex = this.currentIndex - 1;
268 | this._list.splice(idx, 1);
269 | await this.writeMediaList();
270 | return picture;
271 | }
272 | } catch (e) {
273 | console.log("remove in PictureList", e);
274 | }
275 | return null;
276 | }
277 |
278 | async undo() {
279 | if (this.length > 0){
280 | const flag = await this.current.undo();
281 | if(! flag) await this.writeMediaList();
282 | return this.current;
283 | }
284 | return null;
285 | }
286 |
287 | async resize(width, height){
288 | if (this.length > 0){
289 | const flag = await this.current.resize(width, height);
290 | if(! flag) await this.writeMediaList();
291 | return true;
292 | }
293 | return null;
294 | }
295 |
296 | async clockwise(){
297 | if (this.length > 0){
298 | const flag = await this.current.clockwise();
299 | if(! flag) await this.writeMediaList();
300 | return true;
301 | }
302 | return null;
303 | }
304 |
305 | async counterClockwise(){
306 | if (this.length > 0){
307 | const flag = await this.current.counterClockwise();
308 | if(! flag) await this.writeMediaList();
309 | return true;
310 | }
311 | return null;
312 | }
313 |
314 | async crop(originX, originY, width, height){
315 | if (this.length > 0){
316 | const flag = await this.current.crop(originX, originY, width, height);
317 | if(! flag) await this.writeMediaList();
318 | return true;
319 | }
320 | return null;
321 | }
322 |
323 | async verticalFlip(){
324 | if (this.length > 0){
325 | const flag = await this.current.verticalFlip();
326 | if(! flag) await this.writeMediaList();
327 | return true;
328 | }
329 | return null;
330 | }
331 |
332 | async horizontalFlip(){
333 | if (this.length > 0){
334 | const flag = await this.current.horizontalFlip();
335 | if(! flag) await this.writeMediaList();
336 | return true;
337 | }
338 | return null;
339 | }
340 | }
341 |
--------------------------------------------------------------------------------
/src/lib/converters.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This source is from "@ptomasroos/react-native-multi-slider"
3 | * Thank you for sharing a great sauce.
4 | * @copyright Tomas Roos
5 | * @see https://github.com/ptomasroos/react-native-multi-slider/blob/master/converters.js
6 | */
7 |
8 | // Find closest index for a given value
9 | const closest = (array, n) => {
10 | let minI = 0;
11 | let maxI = array.length - 1;
12 |
13 | if (array[minI] > n) {
14 | return minI;
15 | } else if (array[maxI] < n) {
16 | return maxI;
17 | } else if (array[minI] <= n && n <= array[maxI]) {
18 | let closestIndex = null;
19 |
20 | while (closestIndex === null) {
21 | const midI = Math.round((minI + maxI) / 2);
22 | const midVal = array[midI];
23 |
24 | if (midVal === n) {
25 | closestIndex = midI;
26 | } else if (maxI === minI + 1) {
27 | const minValue = array[minI];
28 | const maxValue = array[maxI];
29 | const deltaMin = Math.abs(minValue - n);
30 | const deltaMax = Math.abs(maxValue - n);
31 |
32 | closestIndex = deltaMax <= deltaMin ? maxI : minI;
33 | } else if (midVal < n) {
34 | minI = midI;
35 | } else if (midVal > n) {
36 | maxI = midI;
37 | } else {
38 | closestIndex = -1;
39 | }
40 | }
41 |
42 | return closestIndex;
43 | }
44 |
45 | return -1;
46 | };
47 |
48 | export function valueToPosition(value, valuesArray, sliderLength) {
49 | const index = closest(valuesArray, value);
50 |
51 | const arrLength = valuesArray.length - 1;
52 | const validIndex = index === -1 ? arrLength : index;
53 |
54 | return sliderLength * validIndex / arrLength;
55 | }
56 |
57 | export function positionToValue(position, valuesArray, sliderLength) {
58 | const arrLength = valuesArray.length - 1;
59 |
60 | if (position < 0) {
61 | return valuesArray[0];
62 | } else if (sliderLength < position) {
63 | return valuesArray[arrLength];
64 | } else {
65 | const index = arrLength * position / sliderLength;
66 | return valuesArray[Math.round(index)];
67 | }
68 | }
69 |
70 | export function createArray(start, end, step) {
71 | var i;
72 | var length;
73 | var direction = start - end > 0 ? -1 : 1;
74 | var result = [];
75 | if (!step) {
76 | //console.log('invalid step: ', step);
77 | return result;
78 | } else {
79 | length = Math.abs((start - end) / step) + 1;
80 | for (i = 0; i < length; i++) {
81 | result.push(start + i * Math.abs(step) * direction);
82 | }
83 | return result;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | import FileUtil from './FileUtil';
2 | import Picture from './Picture';
3 | import PictureList from './PictureList';
4 | import Common from './Common';
5 | import { createArray, positionToValue, valueToPosition } from "./converters";
6 |
7 | module.exports = {
8 | Common,
9 | FileUtil,
10 | Picture,
11 | PictureList,
12 | createArray,
13 | positionToValue,
14 | valueToPosition,
15 | };
16 |
--------------------------------------------------------------------------------