diff options
author | Ivan Chen <ivan@tagg.id> | 2021-07-16 21:13:39 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-16 21:13:39 -0400 |
commit | c1b4e35862172b2268a3a53ece0acc807260652e (patch) | |
tree | 40703c8d2dd5abf0a24c07ab8932559ebc2f9cd5 /src/screens/upload/EditMedia.tsx | |
parent | c22c19c9eeb28641d36cb9df38fe95301e0f768c (diff) | |
parent | 4ebb552aef8c5f6136c9359c54f2e4e1aa921241 (diff) |
Merge branch 'master' into tma988-remove-positioned-tags-for-video-moments
Diffstat (limited to 'src/screens/upload/EditMedia.tsx')
-rw-r--r-- | src/screens/upload/EditMedia.tsx | 495 |
1 files changed, 495 insertions, 0 deletions
diff --git a/src/screens/upload/EditMedia.tsx b/src/screens/upload/EditMedia.tsx new file mode 100644 index 00000000..1dc408ee --- /dev/null +++ b/src/screens/upload/EditMedia.tsx @@ -0,0 +1,495 @@ +import ReactNativeZoomableView from '@dudigital/react-native-zoomable-view/src/ReactNativeZoomableView'; +import {RouteProp} from '@react-navigation/core'; +import {StackNavigationProp} from '@react-navigation/stack'; +import React, {useEffect, useRef, useState} from 'react'; +import {Image, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; +import ImageZoom, {IOnMove} from 'react-native-image-pan-zoom'; +import PhotoManipulator from 'react-native-photo-manipulator'; +import TrimIcon from '../../assets/icons/trim.svg'; +import CloseIcon from '../../assets/ionicons/close-outline.svg'; +import {SaveButton, TrimmerPlayer} from '../../components'; +import {TaggLoadingIndicator, TaggSquareButton} from '../../components/common'; +import {MainStackParams} from '../../routes'; +import { + cropVideo, + HeaderHeight, + normalize, + saveImageToGallery, + SCREEN_HEIGHT, + SCREEN_WIDTH, + trimVideo, +} from '../../utils'; + +type EditMediaRouteProps = RouteProp<MainStackParams, 'EditMedia'>; +type EditMediaNavigationProps = StackNavigationProp< + MainStackParams, + 'EditMedia' +>; +interface EditMediaProps { + route: EditMediaRouteProps; + navigation: EditMediaNavigationProps; +} + +export const EditMedia: React.FC<EditMediaProps> = ({route, navigation}) => { + const { + screenType, + selectedCategory, + media: {isVideo}, + } = route.params; + const [aspectRatio, setAspectRatio] = useState<number>(1); + // width and height of video, if video + const [origDimensions, setOrigDimensions] = useState<number[]>([0, 0]); + const [mediaUri, setMediaUri] = useState<string>(route.params.media.uri); + const vidRef = useRef<View>(null); + const [cropLoading, setCropLoading] = useState<boolean>(false); + const [hideTrimmer, setHideTrimmer] = useState<boolean>(true); + + // Stores the coordinates of the cropped image + const [x0, setX0] = useState<number>(); + const [x1, setX1] = useState<number>(); + const [y0, setY0] = useState<number>(); + const [y1, setY1] = useState<number>(); + + // Stores crop information for video + const [videoCrop, setVideoCrop] = useState<{ + cropWidth?: number; + cropHeight?: number; + cropOffsetX?: number; + cropOffsetY?: number; + }>({}); + + // Stores the current trim endpoints + const [trimEnds, setTrimEnds] = useState<{ + end: number; + start: number; + }>({ + end: 60, + start: 0, + }); + + // Setting original aspect ratio of image + useEffect(() => { + if (mediaUri && !isVideo) { + Image.getSize( + mediaUri, + (w, h) => { + setAspectRatio(w / h); + }, + (err) => console.log(err), + ); + } else if (mediaUri && isVideo) { + setVideoCrop((prevState) => ({ + ...prevState, + cropWidth: origDimensions[0], + cropHeight: origDimensions[1], + })); + } + }, []); + + // Possible need to delay setting aspect ratio of video until loaded + useEffect(() => { + if (mediaUri && isVideo) { + setVideoCrop((prevState) => ({ + ...prevState, + cropWidth: origDimensions[0], + cropHeight: origDimensions[1], + })); + } + }, [origDimensions]); + + // Crops original image based of (x0, y0) and (x1, y1) coordinates + const processVideo = (callback: (finalUri: string) => void) => { + if (!isVideo) { + if ( + x0 !== undefined && + x1 !== undefined && + y0 !== undefined && + y1 !== undefined + ) { + PhotoManipulator.crop(mediaUri, { + x: x0, + y: y1, + width: Math.abs(x0 - x1), + height: Math.abs(y0 - y1), + }) + .then((croppedURL) => { + // Pass the cropped image + callback(croppedURL); + }) + .catch((err) => console.log('err: ', err)); + } else if ( + x0 === undefined && + x1 === undefined && + y0 === undefined && + y1 === undefined + ) { + // If no crop coordinates are set, then we will just pass the original image + callback(mediaUri); + } + } else { + if (!videoCrop.cropHeight || !videoCrop.cropWidth) { + setVideoCrop((prevState) => ({ + ...prevState, + cropWidth: origDimensions[0], + cropHeight: origDimensions[1], + })); + } + setCropLoading(true); + cropVideo( + mediaUri, + (croppedURL: string) => { + setCropLoading(false); + // Pass the trimmed/cropped video + callback(croppedURL); + }, + videoCrop, + ); + } + }; + + // for whenever the video is altered by the user + const onVideoMove = (zoomableEvent: any) => { + const {originalHeight, originalWidth} = zoomableEvent; + + let cropWidth = 0; + let cropHeight = 0; + let cropOffsetX = 0; + let cropOffsetY = 0; + + if (vidRef !== null && vidRef.current !== null) { + vidRef.current.measure( + ( + _x: number, + _y: number, + width: number, + height: number, + pageX: number, + pageY: number, + ) => { + // width + cropWidth = origDimensions[0] * (originalWidth / width); + + // offsetX + cropOffsetX = -1 * origDimensions[0] * (pageX / width); + if (cropOffsetX < 0) { + cropOffsetX = 0; + } else if (cropOffsetX + cropWidth > origDimensions[0] - 1) { + cropOffsetX = origDimensions[0] - cropWidth - 1; + } + + // height + if ( + height * (SCREEN_WIDTH / aspectRatio / originalHeight) > + SCREEN_HEIGHT + ) { + const superHeight = width / aspectRatio; + cropHeight = origDimensions[1] * (originalHeight / superHeight); + + // offsetY + const topDeadZone = (height - superHeight) / 2; + const offsetY = topDeadZone + pageY; + cropOffsetY = -1 * origDimensions[1] * (offsetY / superHeight); + if (cropOffsetY < 0) { + cropOffsetY = 0; + } else if (cropOffsetY + cropHeight > origDimensions[1]) { + cropOffsetY = origDimensions[1] - cropHeight - 1; + } + } else { + cropHeight = origDimensions[1]; + } + setVideoCrop((prevState) => ({ + ...prevState, + cropWidth: cropWidth, + cropHeight: cropHeight, + cropOffsetX: cropOffsetX, + cropOffsetY: cropOffsetY, + })); + }, + ); + } + }; + + /* Records (x0, y0) and (x1, y1) coordinates used later for cropping, + * based on(x, y) - the center of the image and scale of zoom + */ + const onMove = (position: IOnMove) => { + Image.getSize( + mediaUri, + (w, h) => { + const x = position.positionX; + const y = position.positionY; + const scale = position.scale; + const screen_ratio = SCREEN_HEIGHT / SCREEN_WIDTH; + let tempx0 = w / 2 - x * (w / SCREEN_WIDTH) - w / 2 / scale; + let tempx1 = w / 2 - x * (w / SCREEN_WIDTH) + w / 2 / scale; + if (tempx0 < 0) { + tempx0 = 0; + } + if (tempx1 > w) { + tempx1 = w; + } + const x_distance = Math.abs(tempx1 - tempx0); + const y_distance = screen_ratio * x_distance; + let tempy0 = h / 2 - y * (h / SCREEN_HEIGHT) + y_distance / 2; + let tempy1 = h / 2 - y * (h / SCREEN_HEIGHT) - y_distance / 2; + if (tempy0 > h) { + tempy0 = h; + } + if (tempy1 < 0) { + tempy1 = 0; + } + setX0(tempx0); + setX1(tempx1); + setY0(tempy0); + setY1(tempy1); + }, + (err) => console.log(err), + ); + }; + + return ( + <View style={styles.container}> + {cropLoading && <TaggLoadingIndicator fullscreen />} + {hideTrimmer && ( + <TouchableOpacity + style={styles.closeButton} + onPress={() => navigation.goBack()}> + <CloseIcon height={25} width={25} color={'white'} /> + </TouchableOpacity> + )} + {!hideTrimmer && ( + <View style={styles.topContainer}> + <TouchableOpacity onPress={() => setHideTrimmer(true)}> + <Text style={styles.bigText}>Cancel</Text> + </TouchableOpacity> + <TouchableOpacity + onPress={() => + trimVideo( + mediaUri, + (trimmedUri: string) => { + setCropLoading(true); + setMediaUri(trimmedUri); + setTimeout(() => { + setHideTrimmer(true); + setCropLoading(false); + }, 500); + }, + trimEnds, + ) + }> + <Text style={styles.bigText}>Save</Text> + </TouchableOpacity> + </View> + )} + {!isVideo ? ( + <ImageZoom + style={styles.zoomView} + cropWidth={SCREEN_WIDTH} + cropHeight={SCREEN_HEIGHT} + imageWidth={SCREEN_WIDTH} + imageHeight={SCREEN_WIDTH / aspectRatio} + onMove={onMove}> + <Image + style={{width: SCREEN_WIDTH, height: SCREEN_WIDTH / aspectRatio}} + source={{ + uri: mediaUri, + }} + /> + </ImageZoom> + ) : ( + <ReactNativeZoomableView + maxZoom={10} + minZoom={1} + zoomStep={0.5} + initialZoom={1} + bindToBorders={true} + zoomEnabled={hideTrimmer} + onDoubleTapAfter={( + _1: any, + _2: any, + zoomableViewEventObject: any, + ) => { + onVideoMove(zoomableViewEventObject); + }} + onShiftingAfter={(_1: any, _2: any, zoomableViewEventObject: any) => { + onVideoMove(zoomableViewEventObject); + }} + onShiftingEnd={(_1: any, _2: any, zoomableViewEventObject: any) => { + onVideoMove(zoomableViewEventObject); + }} + onZoomAfter={(_1: any, _2: any, zoomableViewEventObject: any) => { + onVideoMove(zoomableViewEventObject); + }} + onZoomEnd={(_1: any, _2: any, zoomableViewEventObject: any) => { + onVideoMove(zoomableViewEventObject); + }} + style={styles.zoomView}> + <View style={styles.videoParent} ref={vidRef}> + <TrimmerPlayer + hideTrimmer={hideTrimmer} + source={mediaUri} + videoStyles={[ + styles.media, + { + height: SCREEN_WIDTH / aspectRatio, + }, + ]} + handleLoad={(response: {width: number; height: number}) => { + const {width, height} = response; + setOrigDimensions([width, height]); + setAspectRatio(width / height); + }} + onChangedEndpoints={(response: {start: number; end: number}) => + setTrimEnds(response) + } + /> + </View> + </ReactNativeZoomableView> + )} + {isVideo && hideTrimmer && ( + <View style={styles.iconCarrier}> + <TouchableOpacity + style={styles.iconContainer} + onPress={() => setHideTrimmer((state) => !state)}> + <TrimIcon /> + <Text style={styles.iconText}>Trim</Text> + </TouchableOpacity> + <TouchableOpacity + style={styles.iconContainer} + // TODO: finish me + onPress={() => null}> + <Image + style={styles.volumnIcon} + source={require('../../assets/images/volume-on.png')} + /> + <Text style={styles.iconText}>Volume</Text> + </TouchableOpacity> + </View> + )} + {hideTrimmer && ( + <View style={styles.bottomContainer}> + <SaveButton + style={styles.saveButton} + onPress={() => + processVideo((uri) => + saveImageToGallery(uri, isVideo ? 'video' : 'photo'), + ) + } + /> + <TaggSquareButton + style={styles.button} + onPress={() => + processVideo((uri) => + navigation.navigate('CaptionScreen', { + screenType, + media: { + uri: uri, + isVideo: isVideo, + }, + selectedCategory, + }), + ) + } + title={'Next'} + buttonStyle={'large'} + buttonColor={'blue'} + labelColor={'white'} + labelStyle={styles.buttonLabel} + /> + </View> + )} + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: 'black', + height: SCREEN_HEIGHT, + width: SCREEN_WIDTH, + }, + closeButton: { + position: 'absolute', + top: 0, + paddingTop: HeaderHeight, + zIndex: 1, + marginLeft: '5%', + }, + bottomContainer: { + position: 'absolute', + bottom: SCREEN_HEIGHT * 0.1, + width: SCREEN_WIDTH * 0.8, + justifyContent: 'space-between', + alignItems: 'center', + alignSelf: 'center', + flexDirection: 'row', + }, + topContainer: { + position: 'absolute', + top: SCREEN_HEIGHT * 0.1, + width: SCREEN_WIDTH * 0.9, + justifyContent: 'space-between', + alignItems: 'center', + alignSelf: 'center', + flexDirection: 'row', + zIndex: 1, + }, + bigText: { + fontSize: normalize(15), + color: 'white', + fontWeight: 'bold', + }, + saveButton: { + width: 50, + }, + button: { + width: normalize(108), + height: normalize(36), + }, + buttonLabel: { + fontWeight: '700', + fontSize: normalize(15), + lineHeight: normalize(17.8), + letterSpacing: normalize(1.3), + textAlign: 'center', + }, + iconCarrier: { + width: SCREEN_WIDTH * 0.15, + height: SCREEN_HEIGHT * 0.2, + borderRadius: SCREEN_WIDTH * 0.1, + backgroundColor: 'rgba(0, 0, 0, 0.3)', + position: 'absolute', + right: SCREEN_WIDTH * 0.025, + top: SCREEN_HEIGHT * 0.1, + flexDirection: 'column', + justifyContent: 'space-evenly', + alignItems: 'center', + }, + iconContainer: { + height: 50, + alignItems: 'center', + justifyContent: 'space-between', + }, + iconText: { + color: 'white', + fontSize: normalize(11), + fontWeight: 'bold', + }, + media: { + zIndex: 0, + flex: 1, + }, + videoParent: { + flex: 1, + }, + zoomView: { + backgroundColor: 'black', + flex: 1, + }, + volumnIcon: { + width: 25, + height: 25, + }, +}); + +export default EditMedia; |