| 
									
										
										
										
											2021-02-19 16:07:53 +01:00
										 |  |  | import * as React from 'react'; | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  | import { useRef, useState, useMemo, useCallback } from 'react'; | 
					
						
							|  |  |  | import { StyleSheet, View } from 'react-native'; | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  | import { | 
					
						
							|  |  |  |   PinchGestureHandler, | 
					
						
							|  |  |  |   PinchGestureHandlerGestureEvent, | 
					
						
							|  |  |  |   State, | 
					
						
							|  |  |  |   TapGestureHandler, | 
					
						
							|  |  |  |   TapGestureHandlerStateChangeEvent, | 
					
						
							|  |  |  | } from 'react-native-gesture-handler'; | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  | import { Navigation, NavigationFunctionComponent } from 'react-native-navigation'; | 
					
						
							|  |  |  | import type { CameraDevice, CameraDeviceFormat, CameraProps, CameraRuntimeError, PhotoFile, VideoFile } from 'react-native-vision-camera'; | 
					
						
							| 
									
										
										
										
											2021-02-20 23:20:28 +01:00
										 |  |  | import { Camera, frameRateIncluded, sortDevices, sortFormatsByResolution, filterFormatsByAspectRatio } from 'react-native-vision-camera'; | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  | import { useIsScreenFocused } from './hooks/useIsScreenFocused'; | 
					
						
							| 
									
										
										
										
											2021-02-20 17:39:04 +01:00
										 |  |  | import { CONTENT_SPACING, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING } from './Constants'; | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  | import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from 'react-native-reanimated'; | 
					
						
							|  |  |  | import { useEffect } from 'react'; | 
					
						
							|  |  |  | import { useIsForeground } from './hooks/useIsForeground'; | 
					
						
							|  |  |  | import { StatusBarBlurBackground } from './views/StatusBarBlurBackground'; | 
					
						
							|  |  |  | import { CaptureButton } from './views/CaptureButton'; | 
					
						
							|  |  |  | import { PressableOpacity } from './views/PressableOpacity'; | 
					
						
							|  |  |  | import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons'; | 
					
						
							|  |  |  | import IonIcon from 'react-native-vector-icons/Ionicons'; | 
					
						
							| 
									
										
										
										
											2021-02-20 17:39:04 +01:00
										 |  |  | import { useSelector } from 'pipestate'; | 
					
						
							|  |  |  | import { FpsSelector } from './state/selectors'; | 
					
						
							| 
									
										
										
										
											2021-02-19 16:07:53 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  | const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera); | 
					
						
							|  |  |  | Reanimated.addWhitelistedNativeProps({ | 
					
						
							|  |  |  |   zoom: true, | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const SCALE_FULL_ZOOM = 3; | 
					
						
							|  |  |  | const BUTTON_SIZE = 40; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export const App: NavigationFunctionComponent = ({ componentId }) => { | 
					
						
							|  |  |  |   const camera = useRef<Camera>(null); | 
					
						
							|  |  |  |   const [isCameraInitialized, setIsCameraInitialized] = useState(false); | 
					
						
							|  |  |  |   const zoom = useSharedValue(0); | 
					
						
							|  |  |  |   const isPressingButton = useSharedValue(false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // check if camera page is active
 | 
					
						
							|  |  |  |   const isFocussed = useIsScreenFocused(componentId); | 
					
						
							|  |  |  |   const isForeground = useIsForeground(); | 
					
						
							|  |  |  |   const isActive = isFocussed && isForeground; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |   const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>('back'); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   const [enableHdr, setEnableHdr] = useState(false); | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |   const [flash, setFlash] = useState<'off' | 'on'>('off'); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   const [enableNightMode, setEnableNightMode] = useState(false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // camera format settings
 | 
					
						
							|  |  |  |   const [devices, setDevices] = useState<CameraDevice[]>([]); // All available camera devices, sorted by "best device" (descending)
 | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |   const device = useMemo<CameraDevice | undefined>(() => devices.find((d) => d.position === cameraPosition), [cameraPosition, devices]); | 
					
						
							| 
									
										
										
										
											2021-02-20 23:20:28 +01:00
										 |  |  |   const formats = useMemo<CameraDeviceFormat[]>(() => { | 
					
						
							|  |  |  |     if (device?.formats == null) return []; | 
					
						
							|  |  |  |     const filtered = filterFormatsByAspectRatio(device.formats); | 
					
						
							|  |  |  |     return filtered.sort(sortFormatsByResolution); | 
					
						
							|  |  |  |   }, [device?.formats]); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |   //#region Memos
 | 
					
						
							| 
									
										
										
										
											2021-02-20 17:39:04 +01:00
										 |  |  |   const [targetFps] = useSelector(FpsSelector); | 
					
						
							|  |  |  |   console.log(`Target FPS: ${targetFps}`); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   const fps = useMemo(() => { | 
					
						
							|  |  |  |     if (enableNightMode && !device?.supportsLowLightBoost) { | 
					
						
							|  |  |  |       // User has enabled Night Mode, but Night Mode is not natively supported, so we simulate it by lowering the frame rate.
 | 
					
						
							|  |  |  |       return 30; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-20 17:39:04 +01:00
										 |  |  |     const supportsHdrAtHighFps = formats.some((f) => f.supportsVideoHDR && f.frameRateRanges.some((r) => frameRateIncluded(r, targetFps))); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |     if (enableHdr && !supportsHdrAtHighFps) { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:39:04 +01:00
										 |  |  |       // User has enabled HDR, but HDR is not supported at targetFps.
 | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |       return 30; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-20 17:39:04 +01:00
										 |  |  |     const supportsHighFps = formats.some((f) => f.frameRateRanges.some((r) => frameRateIncluded(r, targetFps))); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |     if (!supportsHighFps) { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:39:04 +01:00
										 |  |  |       // targetFps is not supported by any format.
 | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |       return 30; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-02-20 17:39:04 +01:00
										 |  |  |     // If nothing blocks us from using it, we default to targetFps.
 | 
					
						
							|  |  |  |     return targetFps; | 
					
						
							|  |  |  |   }, [device?.supportsLowLightBoost, enableHdr, enableNightMode, formats, targetFps]); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |   const supportsCameraFlipping = useMemo(() => devices.some((d) => d.position === 'back') && devices.some((d) => d.position === 'front'), [devices]); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   const supportsFlash = device?.hasFlash ?? false; | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |   const supportsHdr = useMemo(() => formats.some((f) => f.supportsVideoHDR), [formats]); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   const canToggleNightMode = enableNightMode | 
					
						
							|  |  |  |     ? true // it's enabled so you have to be able to turn it off again
 | 
					
						
							|  |  |  |     : (device?.supportsLowLightBoost ?? false) || fps > 30; // either we have native support, or we can lower the FPS
 | 
					
						
							|  |  |  |   //#endregion
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const format = useMemo(() => { | 
					
						
							|  |  |  |     let result = formats; | 
					
						
							|  |  |  |     if (enableHdr) { | 
					
						
							|  |  |  |       // We only filter by HDR capable formats if HDR is set to true.
 | 
					
						
							|  |  |  |       // Otherwise we ignore the `supportsVideoHDR` property and accept formats which support HDR `true` or `false`
 | 
					
						
							|  |  |  |       result = result.filter((f) => f.supportsVideoHDR); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-20 23:20:28 +01:00
										 |  |  |     // find the first format that includes the given FPS
 | 
					
						
							|  |  |  |     return result.find((f) => f.frameRateRanges.some((r) => frameRateIncluded(r, fps))); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   }, [formats, fps, enableHdr]); | 
					
						
							| 
									
										
										
										
											2021-02-19 16:07:53 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   //#region Animated Zoom
 | 
					
						
							|  |  |  |   const formatMaxZoom = format?.maxZoom ?? 1; | 
					
						
							|  |  |  |   const maxZoomFactor = Math.min(formatMaxZoom, MAX_ZOOM_FACTOR); | 
					
						
							|  |  |  |   const neutralZoom = device?.neutralZoom ?? 0; | 
					
						
							|  |  |  |   const neutralZoomScaled = (neutralZoom / maxZoomFactor) * formatMaxZoom; | 
					
						
							|  |  |  |   const maxZoomScaled = (1 / formatMaxZoom) * maxZoomFactor; | 
					
						
							| 
									
										
										
										
											2021-02-19 16:07:53 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   const cameraAnimatedProps = useAnimatedProps<Partial<CameraProps>>( | 
					
						
							|  |  |  |     () => ({ | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |       zoom: interpolate(zoom.value, [0, neutralZoomScaled, 1], [0, neutralZoom, maxZoomScaled], Extrapolate.CLAMP), | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |     }), | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     [maxZoomScaled, neutralZoom, neutralZoomScaled, zoom], | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   ); | 
					
						
							|  |  |  |   //#endregion
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   //#region Callbacks
 | 
					
						
							|  |  |  |   const setIsPressingButton = useCallback( | 
					
						
							|  |  |  |     (_isPressingButton: boolean) => { | 
					
						
							|  |  |  |       isPressingButton.value = _isPressingButton; | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     [isPressingButton], | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   ); | 
					
						
							|  |  |  |   // Camera callbacks
 | 
					
						
							|  |  |  |   const onError = useCallback((error: CameraRuntimeError) => { | 
					
						
							|  |  |  |     console.error(error); | 
					
						
							|  |  |  |   }, []); | 
					
						
							|  |  |  |   const onInitialized = useCallback(() => { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     console.log('Camera initialized!'); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |     setIsCameraInitialized(true); | 
					
						
							|  |  |  |   }, []); | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |   const onMediaCaptured = useCallback(async (media: PhotoFile | VideoFile, type: 'photo' | 'video') => { | 
					
						
							|  |  |  |     console.log(`Media captured! ${JSON.stringify(media)}`); | 
					
						
							|  |  |  |     await Navigation.showModal({ | 
					
						
							|  |  |  |       component: { | 
					
						
							|  |  |  |         name: 'Media', | 
					
						
							|  |  |  |         passProps: { | 
					
						
							|  |  |  |           type: type, | 
					
						
							|  |  |  |           path: media.path, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }, []); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   const onFlipCameraPressed = useCallback(() => { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     setCameraPosition((p) => (p === 'back' ? 'front' : 'back')); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   }, []); | 
					
						
							|  |  |  |   const onHdrSwitchPressed = useCallback(() => { | 
					
						
							|  |  |  |     setEnableHdr((h) => !h); | 
					
						
							|  |  |  |   }, []); | 
					
						
							|  |  |  |   const onFlashPressed = useCallback(() => { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     setFlash((f) => (f === 'off' ? 'on' : 'off')); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   }, []); | 
					
						
							|  |  |  |   const onNightModePressed = useCallback(() => { | 
					
						
							|  |  |  |     setEnableNightMode((n) => !n); | 
					
						
							|  |  |  |   }, []); | 
					
						
							| 
									
										
										
										
											2021-02-19 20:05:02 +01:00
										 |  |  |   const onSettingsPressed = useCallback(() => { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     Navigation.push(componentId, { component: { name: 'Settings' } }); | 
					
						
							| 
									
										
										
										
											2021-02-19 20:05:02 +01:00
										 |  |  |   }, [componentId]); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   //#endregion
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   //#region Tap Gesture
 | 
					
						
							|  |  |  |   const onDoubleTapGesture = useCallback( | 
					
						
							|  |  |  |     ({ nativeEvent: event }: TapGestureHandlerStateChangeEvent) => { | 
					
						
							|  |  |  |       // TODO: (MARC) Allow switching camera (back <-> front) while recording and stich videos together!
 | 
					
						
							|  |  |  |       if (isPressingButton.value) return; | 
					
						
							|  |  |  |       switch (event.state) { | 
					
						
							|  |  |  |         case State.END: | 
					
						
							|  |  |  |           // on double tap
 | 
					
						
							|  |  |  |           onFlipCameraPressed(); | 
					
						
							|  |  |  |           break; | 
					
						
							|  |  |  |         default: | 
					
						
							|  |  |  |           break; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     [isPressingButton, onFlipCameraPressed], | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   ); | 
					
						
							|  |  |  |   //#endregion
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   //#region Effects
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:08:35 +01:00
										 |  |  |     const loadDevices = async (): Promise<void> => { | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |       try { | 
					
						
							|  |  |  |         const availableCameraDevices = await Camera.getAvailableCameraDevices(); | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |         console.log(`Devices: ${availableCameraDevices.map((d) => d.name).join(', ')}`); | 
					
						
							| 
									
										
										
										
											2021-02-20 23:20:28 +01:00
										 |  |  |         const sortedDevices = availableCameraDevices.sort(sortDevices); | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |         console.debug(`Devices (sorted): ${sortedDevices.map((d) => d.name).join(', ')}`); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |         setDevices(sortedDevices); | 
					
						
							|  |  |  |       } catch (e) { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |         console.error('Failed to get available devices!', e); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |       } | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     loadDevices(); | 
					
						
							|  |  |  |   }, []); | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     // Run everytime the neutralZoomScaled value changes. (reset zoom when device changes)
 | 
					
						
							|  |  |  |     zoom.value = neutralZoomScaled; | 
					
						
							|  |  |  |   }, [neutralZoomScaled, zoom]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     // Run everytime the camera gets set to isActive = false. (reset zoom when tab switching)
 | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     if (!isActive) zoom.value = neutralZoomScaled; | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   }, [neutralZoomScaled, isActive, zoom]); | 
					
						
							|  |  |  |   //#endregion
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   //#region Pinch to Zoom Gesture
 | 
					
						
							|  |  |  |   // The gesture handler maps the linear pinch gesture (0 - 1) to an exponential curve since a camera's zoom
 | 
					
						
							|  |  |  |   // function does not appear linear to the user. (aka zoom 0.1 -> 0.2 does not look equal in difference as 0.8 -> 0.9)
 | 
					
						
							|  |  |  |   const onPinchGesture = useAnimatedGestureHandler<PinchGestureHandlerGestureEvent, { startZoom?: number }>({ | 
					
						
							|  |  |  |     onStart: (_, context) => { | 
					
						
							|  |  |  |       context.startZoom = zoom.value; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     onActive: (event, context) => { | 
					
						
							|  |  |  |       // we're trying to map the scale gesture to a linear zoom here
 | 
					
						
							|  |  |  |       const startZoom = context.startZoom ?? 0; | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |       const scale = interpolate(event.scale, [1 - 1 / SCALE_FULL_ZOOM, 1, SCALE_FULL_ZOOM], [-1, 0, 1], Extrapolate.CLAMP); | 
					
						
							|  |  |  |       zoom.value = interpolate(scale, [-1, 0, 1], [0, startZoom, 1], Extrapolate.CLAMP); | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |     }, | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   //#endregion
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-19 20:05:02 +01:00
										 |  |  |   if (device != null && format != null) { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     console.log( | 
					
						
							|  |  |  |       `Re-rendering camera page with ${isActive ? 'active' : 'inactive'} camera. ` + | 
					
						
							|  |  |  |         `Device: "${device.name}" (${format.photoWidth}x${format.photoHeight} @ ${fps}fps)`, | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2021-02-19 20:05:02 +01:00
										 |  |  |   } else { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     console.log('re-rendering camera page without active camera'); | 
					
						
							| 
									
										
										
										
											2021-02-19 20:05:02 +01:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   // TODO: Implement camera flipping (back <-> front) while recording and stich the videos together
 | 
					
						
							|  |  |  |   // TODO: iOS: Use custom video data stream output to manually process the data and write the MOV/MP4 for more customizability.
 | 
					
						
							| 
									
										
										
										
											2021-02-19 16:07:53 +01:00
										 |  |  |   return ( | 
					
						
							|  |  |  |     <View style={styles.container}> | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |       {device != null && ( | 
					
						
							|  |  |  |         <PinchGestureHandler onGestureEvent={onPinchGesture} enabled={isActive}> | 
					
						
							|  |  |  |           <Reanimated.View style={StyleSheet.absoluteFill}> | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |             <TapGestureHandler onHandlerStateChange={onDoubleTapGesture} numberOfTaps={2}> | 
					
						
							| 
									
										
										
										
											2021-02-19 20:05:02 +01:00
										 |  |  |               <ReanimatedCamera | 
					
						
							|  |  |  |                 ref={camera} | 
					
						
							|  |  |  |                 style={StyleSheet.absoluteFill} | 
					
						
							|  |  |  |                 device={device} | 
					
						
							|  |  |  |                 format={format} | 
					
						
							|  |  |  |                 fps={fps} | 
					
						
							|  |  |  |                 hdr={enableHdr} | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |                 lowLightBoost={device.supportsLowLightBoost && enableNightMode} | 
					
						
							| 
									
										
										
										
											2021-02-19 20:05:02 +01:00
										 |  |  |                 isActive={isActive} | 
					
						
							|  |  |  |                 onInitialized={onInitialized} | 
					
						
							|  |  |  |                 onError={onError} | 
					
						
							|  |  |  |                 enableZoomGesture={false} | 
					
						
							|  |  |  |                 // TODO: Remove once https://github.com/software-mansion/react-native-reanimated/pull/1697 gets merged
 | 
					
						
							|  |  |  |                 // @ts-expect-error animatedProps should be Partial<P>
 | 
					
						
							|  |  |  |                 animatedProps={cameraAnimatedProps} | 
					
						
							|  |  |  |               /> | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |             </TapGestureHandler> | 
					
						
							|  |  |  |           </Reanimated.View> | 
					
						
							|  |  |  |         </PinchGestureHandler> | 
					
						
							|  |  |  |       )} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       <CaptureButton | 
					
						
							|  |  |  |         style={styles.captureButton} | 
					
						
							|  |  |  |         camera={camera} | 
					
						
							|  |  |  |         onMediaCaptured={onMediaCaptured} | 
					
						
							|  |  |  |         cameraZoom={zoom} | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |         flash={supportsFlash ? flash : 'off'} | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |         enabled={isCameraInitialized && isActive} | 
					
						
							|  |  |  |         setIsPressingButton={setIsPressingButton} | 
					
						
							|  |  |  |       /> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       <StatusBarBlurBackground /> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       <View style={styles.rightButtonRow}> | 
					
						
							|  |  |  |         {supportsCameraFlipping && ( | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |           <PressableOpacity style={styles.button} onPress={onFlipCameraPressed} disabledOpacity={0.4}> | 
					
						
							|  |  |  |             <IonIcon name="camera-reverse" color="white" size={24} /> | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |           </PressableOpacity> | 
					
						
							|  |  |  |         )} | 
					
						
							|  |  |  |         {supportsFlash && ( | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |           <PressableOpacity style={styles.button} onPress={onFlashPressed} disabledOpacity={0.4}> | 
					
						
							|  |  |  |             <IonIcon name={flash === 'on' ? 'flash' : 'flash-off'} color="white" size={24} /> | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |           </PressableOpacity> | 
					
						
							|  |  |  |         )} | 
					
						
							|  |  |  |         {canToggleNightMode && ( | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |           <PressableOpacity style={styles.button} onPress={onNightModePressed} disabledOpacity={0.4}> | 
					
						
							|  |  |  |             <IonIcon name={enableNightMode ? 'moon' : 'moon-outline'} color="white" size={24} /> | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |           </PressableOpacity> | 
					
						
							|  |  |  |         )} | 
					
						
							|  |  |  |         {supportsHdr && ( | 
					
						
							|  |  |  |           <PressableOpacity style={styles.button} onPress={onHdrSwitchPressed}> | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |             <MaterialIcon name={enableHdr ? 'hdr' : 'hdr-off'} color="white" size={24} /> | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |           </PressableOpacity> | 
					
						
							|  |  |  |         )} | 
					
						
							| 
									
										
										
										
											2021-02-19 20:05:02 +01:00
										 |  |  |         <PressableOpacity style={styles.button} onPress={onSettingsPressed}> | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |           <IonIcon name="settings-outline" color="white" size={24} /> | 
					
						
							| 
									
										
										
										
											2021-02-19 20:05:02 +01:00
										 |  |  |         </PressableOpacity> | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |       </View> | 
					
						
							| 
									
										
										
										
											2021-02-19 16:07:53 +01:00
										 |  |  |     </View> | 
					
						
							|  |  |  |   ); | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2021-02-19 16:07:53 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | const styles = StyleSheet.create({ | 
					
						
							|  |  |  |   container: { | 
					
						
							|  |  |  |     flex: 1, | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     backgroundColor: 'black', | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   }, | 
					
						
							|  |  |  |   captureButton: { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     position: 'absolute', | 
					
						
							|  |  |  |     alignSelf: 'center', | 
					
						
							|  |  |  |     bottom: SAFE_AREA_PADDING.paddingBottom, | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   }, | 
					
						
							|  |  |  |   button: { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:42:37 +01:00
										 |  |  |     marginBottom: CONTENT_SPACING, | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |     width: BUTTON_SIZE, | 
					
						
							|  |  |  |     height: BUTTON_SIZE, | 
					
						
							|  |  |  |     borderRadius: BUTTON_SIZE / 2, | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     backgroundColor: 'rgba(140, 140, 140, 0.3)', | 
					
						
							|  |  |  |     justifyContent: 'center', | 
					
						
							|  |  |  |     alignItems: 'center', | 
					
						
							| 
									
										
										
										
											2021-02-19 16:07:53 +01:00
										 |  |  |   }, | 
					
						
							| 
									
										
										
										
											2021-02-19 19:06:28 +01:00
										 |  |  |   rightButtonRow: { | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     position: 'absolute', | 
					
						
							| 
									
										
										
										
											2021-02-20 17:42:37 +01:00
										 |  |  |     right: SAFE_AREA_PADDING.paddingRight, | 
					
						
							| 
									
										
										
										
											2021-02-20 17:07:10 +01:00
										 |  |  |     top: SAFE_AREA_PADDING.paddingTop, | 
					
						
							| 
									
										
										
										
											2021-02-19 16:07:53 +01:00
										 |  |  |   }, | 
					
						
							|  |  |  | }); |