diff options
Diffstat (limited to 'src/screens')
| -rw-r--r-- | src/screens/index.ts | 1 | ||||
| -rw-r--r-- | src/screens/moments/CameraScreen.tsx | 121 | ||||
| -rw-r--r-- | src/screens/profile/CaptionScreen.tsx | 2 | ||||
| -rw-r--r-- | src/screens/upload/EditMedia.tsx | 495 | ||||
| -rw-r--r-- | src/screens/upload/index.ts | 1 |
5 files changed, 537 insertions, 83 deletions
diff --git a/src/screens/index.ts b/src/screens/index.ts index 0c7d911f..5fa14d05 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -7,3 +7,4 @@ export * from './suggestedPeopleOnboarding'; export * from './badge'; export * from './chat'; export * from './moments'; +export * from './upload'; diff --git a/src/screens/moments/CameraScreen.tsx b/src/screens/moments/CameraScreen.tsx index ee5834cb..33ee2347 100644 --- a/src/screens/moments/CameraScreen.tsx +++ b/src/screens/moments/CameraScreen.tsx @@ -8,16 +8,10 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native'; import {CameraType, FlashMode, RNCamera} from 'react-native-camera'; import {AnimatedCircularProgress} from 'react-native-circular-progress'; import CloseIcon from '../../assets/ionicons/close-outline.svg'; -import { - FlashButton, - FlipButton, - GalleryIcon, - SaveButton, - TaggSquareButton, -} from '../../components'; +import {FlashButton, FlipButton, GalleryIcon} from '../../components'; import {TAGG_PURPLE} from '../../constants'; import {MainStackParams} from '../../routes'; -import {HeaderHeight, normalize, SCREEN_WIDTH} from '../../utils'; +import {HeaderHeight, SCREEN_WIDTH} from '../../utils'; import {showGIFFailureAlert, takePicture, takeVideo} from '../../utils/camera'; type CameraScreenRouteProps = RouteProp<MainStackParams, 'CameraScreen'>; @@ -35,9 +29,7 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { const tabBarHeight = useBottomTabBarHeight(); const [cameraType, setCameraType] = useState<keyof CameraType>('front'); const [flashMode, setFlashMode] = useState<keyof FlashMode>('off'); - const [mediaFromGallery, setMediaFromGallery] = useState<string>(''); const [mostRecentPhoto, setMostRecentPhoto] = useState<string>(''); - const [showSaveButton, setShowSaveButton] = useState<boolean>(false); const [isRecording, setIsRecording] = useState<boolean>(false); useFocusEffect( @@ -62,14 +54,21 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { .catch((_err) => console.log('Unable to fetch preview photo for gallery'), ); - }, [mediaFromGallery]); + }, []); - const navigateToCropper = (uri: string) => { - navigation.navigate('ZoomInCropper', { + const navigateToEditMedia = (uri: string) => { + navigation.navigate('EditMedia', { screenType, media: { uri, - isVideo: false, + isVideo: !( + uri.endsWith('jpg') || + uri.endsWith('JPG') || + uri.endsWith('PNG') || + uri.endsWith('png') || + uri.endsWith('GIF') || + uri.endsWith('gif') + ), }, selectedCategory, }); @@ -85,21 +84,11 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { }); }; - /* - * If picture is not taken yet, exists from camera screen to profile view - * If picture is taken, exists from captured image's preview to camera - * */ const handleClose = () => { - if (showSaveButton) { - cameraRef.current?.resumePreview(); - setShowSaveButton(false); - setMediaFromGallery(''); - } else { - navigation.dangerouslyGetParent()?.setOptions({ - tabBarVisible: true, - }); - navigation.goBack(); - } + navigation.dangerouslyGetParent()?.setOptions({ + tabBarVisible: true, + }); + navigation.goBack(); }; return ( @@ -118,12 +107,13 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { }} /> <View style={[styles.bottomContainer, {bottom: tabBarHeight}]}> - {showSaveButton ? ( - <SaveButton capturedImageURI={mediaFromGallery} /> - ) : ( - <FlipButton cameraType={cameraType} setCameraType={setCameraType} /> - )} + <FlipButton cameraType={cameraType} setCameraType={setCameraType} /> <TouchableOpacity + style={ + isRecording + ? styles.captureButtonVideoContainer + : styles.captureButtonContainer + } activeOpacity={1} onLongPress={() => { takeVideo(cameraRef, (vid) => { @@ -138,16 +128,8 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { } }} onPress={() => { - takePicture(cameraRef, (pic) => { - setShowSaveButton(true); - setMediaFromGallery(pic.uri); - }); - }} - style={ - isRecording - ? styles.captureButtonVideoContainer - : styles.captureButtonContainer - }> + takePicture(cameraRef, (pic) => navigateToEditMedia(pic.uri)); + }}> <View style={styles.captureButton} /> </TouchableOpacity> {isRecording && ( @@ -163,32 +145,20 @@ const CameraScreen: React.FC<CameraScreenProps> = ({route, navigation}) => { /> )} <View style={styles.bottomRightContainer}> - {mediaFromGallery ? ( - <TaggSquareButton - onPress={() => navigateToCaptionScreen(false, mediaFromGallery)} - title={'Next'} - buttonStyle={'large'} - buttonColor={'blue'} - labelColor={'white'} - style={styles.nextButton} - labelStyle={styles.nextButtonLabel} - /> - ) : ( - <GalleryIcon - mostRecentPhotoUri={mostRecentPhoto} - callback={(media) => { - const filename = media.filename; - if ( - filename && - (filename.endsWith('gif') || filename.endsWith('GIF')) - ) { - showGIFFailureAlert(() => navigateToCropper(media.path)); - } else { - navigateToCropper(media.path); - } - }} - /> - )} + <GalleryIcon + mostRecentPhotoUri={mostRecentPhoto} + callback={(media) => { + const filename = media.filename; + if ( + filename && + (filename.endsWith('gif') || filename.endsWith('GIF')) + ) { + showGIFFailureAlert(() => navigateToEditMedia(media.path)); + } else { + navigateToEditMedia(media.path); + } + }} + /> </View> </View> </View> @@ -254,19 +224,6 @@ const styles = StyleSheet.create({ alignItems: 'center', width: (SCREEN_WIDTH - 100) / 2, }, - nextButton: { - zIndex: 1, - width: normalize(100), - height: normalize(37), - borderRadius: 10, - }, - nextButtonLabel: { - fontWeight: '700', - fontSize: normalize(15), - lineHeight: normalize(17.8), - letterSpacing: normalize(1.3), - textAlign: 'center', - }, }); export default CameraScreen; diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx index d07743ad..7f77bdca 100644 --- a/src/screens/profile/CaptionScreen.tsx +++ b/src/screens/profile/CaptionScreen.tsx @@ -49,7 +49,6 @@ import {RootState} from '../../store/rootReducer'; import {MomentTagType} from '../../types'; import {isIPhoneX, normalize, SCREEN_WIDTH, StatusBarHeight} from '../../utils'; import {mentionPartTypes} from '../../utils/comments'; - /** * Upload Screen to allow users to upload posts to Tagg */ @@ -362,6 +361,7 @@ const CaptionScreen: React.FC<CaptionScreenProps> = ({route, navigation}) => { </SearchBackground> ); }; + const styles = StyleSheet.create({ contentContainer: { paddingTop: StatusBarHeight, 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; diff --git a/src/screens/upload/index.ts b/src/screens/upload/index.ts new file mode 100644 index 00000000..0dadeede --- /dev/null +++ b/src/screens/upload/index.ts @@ -0,0 +1 @@ +export {default as EditMedia} from './EditMedia'; |
