chore: Remove semicolons (#1846)
* chore: Disable `semi` in Prettier * chore: Format w/o semi * Remove more `;` * Lint example * More ;
This commit is contained in:
@@ -13,4 +13,4 @@ module.exports = {
|
||||
ignorePatterns: ['babel.config.js', 'metro.config.js', '.eslintrc.js'],
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: ['plugin:@typescript-eslint/recommended', '@react-native', '../.eslintrc.js'],
|
||||
};
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const path = require('path');
|
||||
const pak = require('../package.json');
|
||||
const path = require('path')
|
||||
const pak = require('../package.json')
|
||||
|
||||
module.exports = {
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
@@ -16,4 +16,4 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import { AppRegistry } from 'react-native';
|
||||
import { App } from './src/App';
|
||||
import 'react-native-gesture-handler'
|
||||
import { AppRegistry } from 'react-native'
|
||||
import { App } from './src/App'
|
||||
|
||||
AppRegistry.registerComponent('VisionCameraExample', () => App);
|
||||
AppRegistry.registerComponent('VisionCameraExample', () => App)
|
||||
|
@@ -1,11 +1,11 @@
|
||||
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
|
||||
const path = require('path');
|
||||
const escape = require('escape-string-regexp');
|
||||
const exclusionList = require('metro-config/src/defaults/exclusionList');
|
||||
const pak = require('../package.json');
|
||||
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')
|
||||
const path = require('path')
|
||||
const escape = require('escape-string-regexp')
|
||||
const exclusionList = require('metro-config/src/defaults/exclusionList')
|
||||
const pak = require('../package.json')
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const modules = Object.keys({ ...pak.peerDependencies });
|
||||
const root = path.resolve(__dirname, '..')
|
||||
const modules = Object.keys({ ...pak.peerDependencies })
|
||||
|
||||
/**
|
||||
* Metro configuration
|
||||
@@ -27,8 +27,8 @@ const config = {
|
||||
),
|
||||
|
||||
extraNodeModules: modules.reduce((acc, name) => {
|
||||
acc[name] = path.join(__dirname, 'node_modules', name);
|
||||
return acc;
|
||||
acc[name] = path.join(__dirname, 'node_modules', name)
|
||||
return acc
|
||||
}, {}),
|
||||
},
|
||||
|
||||
@@ -40,6 +40,6 @@ const config = {
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
||||
module.exports = mergeConfig(getDefaultConfig(__dirname), config)
|
||||
|
@@ -8,7 +8,7 @@
|
||||
"ios": "react-native run-ios",
|
||||
"start": "react-native start",
|
||||
"pods": "cd ios && pod install",
|
||||
"lint": "eslint .",
|
||||
"lint": "eslint . --fix",
|
||||
"lint-ci": "yarn lint -f ../node_modules/@firmnav/eslint-github-actions-formatter/dist/formatter.js",
|
||||
"typescript": "tsc --noEmit"
|
||||
},
|
||||
|
@@ -1,32 +1,32 @@
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { PermissionsPage } from './PermissionsPage';
|
||||
import { MediaPage } from './MediaPage';
|
||||
import { CameraPage } from './CameraPage';
|
||||
import type { Routes } from './Routes';
|
||||
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { NavigationContainer } from '@react-navigation/native'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import { PermissionsPage } from './PermissionsPage'
|
||||
import { MediaPage } from './MediaPage'
|
||||
import { CameraPage } from './CameraPage'
|
||||
import type { Routes } from './Routes'
|
||||
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera'
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
||||
|
||||
const Stack = createNativeStackNavigator<Routes>();
|
||||
const Stack = createNativeStackNavigator<Routes>()
|
||||
|
||||
export function App(): React.ReactElement | null {
|
||||
const [cameraPermission, setCameraPermission] = useState<CameraPermissionStatus>();
|
||||
const [microphonePermission, setMicrophonePermission] = useState<CameraPermissionStatus>();
|
||||
const [cameraPermission, setCameraPermission] = useState<CameraPermissionStatus>()
|
||||
const [microphonePermission, setMicrophonePermission] = useState<CameraPermissionStatus>()
|
||||
|
||||
useEffect(() => {
|
||||
Camera.getCameraPermissionStatus().then(setCameraPermission);
|
||||
Camera.getMicrophonePermissionStatus().then(setMicrophonePermission);
|
||||
}, []);
|
||||
Camera.getCameraPermissionStatus().then(setCameraPermission)
|
||||
Camera.getMicrophonePermissionStatus().then(setMicrophonePermission)
|
||||
}, [])
|
||||
|
||||
console.log(`Re-rendering Navigator. Camera: ${cameraPermission} | Microphone: ${microphonePermission}`);
|
||||
console.log(`Re-rendering Navigator. Camera: ${cameraPermission} | Microphone: ${microphonePermission}`)
|
||||
|
||||
if (cameraPermission == null || microphonePermission == null) {
|
||||
// still loading
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const showPermissionsPage = cameraPermission !== 'granted' || microphonePermission === 'not-determined';
|
||||
const showPermissionsPage = cameraPermission !== 'granted' || microphonePermission === 'not-determined'
|
||||
return (
|
||||
<NavigationContainer>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
@@ -50,5 +50,5 @@ export function App(): React.ReactElement | null {
|
||||
</Stack.Navigator>
|
||||
</GestureHandlerRootView>
|
||||
</NavigationContainer>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
@@ -1,135 +1,135 @@
|
||||
import * as React from 'react';
|
||||
import { useRef, useState, useCallback, useMemo } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler';
|
||||
import { CameraRuntimeError, PhotoFile, useCameraDevice, useCameraFormat, useFrameProcessor, VideoFile } from 'react-native-vision-camera';
|
||||
import { Camera } from 'react-native-vision-camera';
|
||||
import { CONTENT_SPACING, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING, SCREEN_HEIGHT, SCREEN_WIDTH } from './Constants';
|
||||
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 'react-native-pressable-opacity';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
import type { Routes } from './Routes';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { useIsFocused } from '@react-navigation/core';
|
||||
import { examplePlugin } from './frame-processors/ExamplePlugin';
|
||||
import * as React from 'react'
|
||||
import { useRef, useState, useCallback, useMemo } from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler'
|
||||
import { CameraRuntimeError, PhotoFile, useCameraDevice, useCameraFormat, useFrameProcessor, VideoFile } from 'react-native-vision-camera'
|
||||
import { Camera } from 'react-native-vision-camera'
|
||||
import { CONTENT_SPACING, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING, SCREEN_HEIGHT, SCREEN_WIDTH } from './Constants'
|
||||
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 'react-native-pressable-opacity'
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons'
|
||||
import type { Routes } from './Routes'
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
|
||||
import { useIsFocused } from '@react-navigation/core'
|
||||
import { examplePlugin } from './frame-processors/ExamplePlugin'
|
||||
|
||||
const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera);
|
||||
const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera)
|
||||
Reanimated.addWhitelistedNativeProps({
|
||||
zoom: true,
|
||||
});
|
||||
})
|
||||
|
||||
const SCALE_FULL_ZOOM = 3;
|
||||
const BUTTON_SIZE = 40;
|
||||
const SCALE_FULL_ZOOM = 3
|
||||
const BUTTON_SIZE = 40
|
||||
|
||||
type Props = NativeStackScreenProps<Routes, 'CameraPage'>;
|
||||
type Props = NativeStackScreenProps<Routes, 'CameraPage'>
|
||||
export function CameraPage({ navigation }: Props): React.ReactElement {
|
||||
const camera = useRef<Camera>(null);
|
||||
const [isCameraInitialized, setIsCameraInitialized] = useState(false);
|
||||
const [hasMicrophonePermission, setHasMicrophonePermission] = useState(false);
|
||||
const zoom = useSharedValue(0);
|
||||
const isPressingButton = useSharedValue(false);
|
||||
const camera = useRef<Camera>(null)
|
||||
const [isCameraInitialized, setIsCameraInitialized] = useState(false)
|
||||
const [hasMicrophonePermission, setHasMicrophonePermission] = useState(false)
|
||||
const zoom = useSharedValue(0)
|
||||
const isPressingButton = useSharedValue(false)
|
||||
|
||||
// check if camera page is active
|
||||
const isFocussed = useIsFocused();
|
||||
const isForeground = useIsForeground();
|
||||
const isActive = isFocussed && isForeground;
|
||||
const isFocussed = useIsFocused()
|
||||
const isForeground = useIsForeground()
|
||||
const isActive = isFocussed && isForeground
|
||||
|
||||
const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>('back');
|
||||
const [enableHdr, setEnableHdr] = useState(false);
|
||||
const [flash, setFlash] = useState<'off' | 'on'>('off');
|
||||
const [enableNightMode, setEnableNightMode] = useState(false);
|
||||
const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>('back')
|
||||
const [enableHdr, setEnableHdr] = useState(false)
|
||||
const [flash, setFlash] = useState<'off' | 'on'>('off')
|
||||
const [enableNightMode, setEnableNightMode] = useState(false)
|
||||
|
||||
// camera format settings
|
||||
const device = useCameraDevice(cameraPosition, {
|
||||
physicalDevices: ['ultra-wide-angle-camera', 'wide-angle-camera', 'telephoto-camera'],
|
||||
});
|
||||
})
|
||||
|
||||
const [targetFps, setTargetFps] = useState(60);
|
||||
const [targetFps, setTargetFps] = useState(60)
|
||||
|
||||
const screenAspectRatio = SCREEN_HEIGHT / SCREEN_WIDTH;
|
||||
const screenAspectRatio = SCREEN_HEIGHT / SCREEN_WIDTH
|
||||
const format = useCameraFormat(device, [
|
||||
{ fps: targetFps },
|
||||
{ videoAspectRatio: screenAspectRatio },
|
||||
{ videoResolution: 'max' },
|
||||
{ photoAspectRatio: screenAspectRatio },
|
||||
{ photoResolution: 'max' },
|
||||
]);
|
||||
])
|
||||
|
||||
const fps = Math.min(format?.maxFps ?? 1, targetFps);
|
||||
const fps = Math.min(format?.maxFps ?? 1, targetFps)
|
||||
|
||||
const supportsFlash = device?.hasFlash ?? false;
|
||||
const supportsHdr = format?.supportsPhotoHDR;
|
||||
const supports60Fps = useMemo(() => device?.formats.some((f) => f.maxFps >= 60), [device?.formats]);
|
||||
const canToggleNightMode = device?.supportsLowLightBoost ?? false;
|
||||
const supportsFlash = device?.hasFlash ?? false
|
||||
const supportsHdr = format?.supportsPhotoHDR
|
||||
const supports60Fps = useMemo(() => device?.formats.some((f) => f.maxFps >= 60), [device?.formats])
|
||||
const canToggleNightMode = device?.supportsLowLightBoost ?? false
|
||||
|
||||
//#region Animated Zoom
|
||||
// This just maps the zoom factor to a percentage value.
|
||||
// so e.g. for [min, neutr., max] values [1, 2, 128] this would result in [0, 0.0081, 1]
|
||||
const minZoom = device?.minZoom ?? 1;
|
||||
const maxZoom = Math.min(device?.maxZoom ?? 1, MAX_ZOOM_FACTOR);
|
||||
const minZoom = device?.minZoom ?? 1
|
||||
const maxZoom = Math.min(device?.maxZoom ?? 1, MAX_ZOOM_FACTOR)
|
||||
|
||||
const cameraAnimatedProps = useAnimatedProps(() => {
|
||||
const z = Math.max(Math.min(zoom.value, maxZoom), minZoom);
|
||||
const z = Math.max(Math.min(zoom.value, maxZoom), minZoom)
|
||||
return {
|
||||
zoom: z,
|
||||
};
|
||||
}, [maxZoom, minZoom, zoom]);
|
||||
}
|
||||
}, [maxZoom, minZoom, zoom])
|
||||
//#endregion
|
||||
|
||||
//#region Callbacks
|
||||
const setIsPressingButton = useCallback(
|
||||
(_isPressingButton: boolean) => {
|
||||
isPressingButton.value = _isPressingButton;
|
||||
isPressingButton.value = _isPressingButton
|
||||
},
|
||||
[isPressingButton],
|
||||
);
|
||||
)
|
||||
// Camera callbacks
|
||||
const onError = useCallback((error: CameraRuntimeError) => {
|
||||
console.error(error);
|
||||
}, []);
|
||||
console.error(error)
|
||||
}, [])
|
||||
const onInitialized = useCallback(() => {
|
||||
console.log('Camera initialized!');
|
||||
setIsCameraInitialized(true);
|
||||
}, []);
|
||||
console.log('Camera initialized!')
|
||||
setIsCameraInitialized(true)
|
||||
}, [])
|
||||
const onMediaCaptured = useCallback(
|
||||
(media: PhotoFile | VideoFile, type: 'photo' | 'video') => {
|
||||
console.log(`Media captured! ${JSON.stringify(media)}`);
|
||||
console.log(`Media captured! ${JSON.stringify(media)}`)
|
||||
navigation.navigate('MediaPage', {
|
||||
path: media.path,
|
||||
type: type,
|
||||
});
|
||||
})
|
||||
},
|
||||
[navigation],
|
||||
);
|
||||
)
|
||||
const onFlipCameraPressed = useCallback(() => {
|
||||
setCameraPosition((p) => (p === 'back' ? 'front' : 'back'));
|
||||
}, []);
|
||||
setCameraPosition((p) => (p === 'back' ? 'front' : 'back'))
|
||||
}, [])
|
||||
const onFlashPressed = useCallback(() => {
|
||||
setFlash((f) => (f === 'off' ? 'on' : 'off'));
|
||||
}, []);
|
||||
setFlash((f) => (f === 'off' ? 'on' : 'off'))
|
||||
}, [])
|
||||
//#endregion
|
||||
|
||||
//#region Tap Gesture
|
||||
const onDoubleTap = useCallback(() => {
|
||||
onFlipCameraPressed();
|
||||
}, [onFlipCameraPressed]);
|
||||
onFlipCameraPressed()
|
||||
}, [onFlipCameraPressed])
|
||||
//#endregion
|
||||
|
||||
//#region Effects
|
||||
const neutralZoom = device?.neutralZoom ?? 1;
|
||||
const neutralZoom = device?.neutralZoom ?? 1
|
||||
useEffect(() => {
|
||||
// Run everytime the neutralZoomScaled value changes. (reset zoom when device changes)
|
||||
zoom.value = neutralZoom;
|
||||
}, [neutralZoom, zoom]);
|
||||
zoom.value = neutralZoom
|
||||
}, [neutralZoom, zoom])
|
||||
|
||||
useEffect(() => {
|
||||
Camera.getMicrophonePermissionStatus().then((status) => setHasMicrophonePermission(status === 'granted'));
|
||||
}, []);
|
||||
Camera.getMicrophonePermissionStatus().then((status) => setHasMicrophonePermission(status === 'granted'))
|
||||
}, [])
|
||||
//#endregion
|
||||
|
||||
//#region Pinch to Zoom Gesture
|
||||
@@ -137,32 +137,32 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
|
||||
// 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;
|
||||
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;
|
||||
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], [minZoom, startZoom, maxZoom], Extrapolate.CLAMP);
|
||||
const startZoom = context.startZoom ?? 0
|
||||
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], [minZoom, startZoom, maxZoom], Extrapolate.CLAMP)
|
||||
},
|
||||
});
|
||||
})
|
||||
//#endregion
|
||||
|
||||
if (device != null && format != null) {
|
||||
console.log(
|
||||
`Re-rendering camera page with ${isActive ? 'active' : 'inactive'} camera. ` +
|
||||
`Device: "${device.name}" (${format.photoWidth}x${format.photoHeight} photo / ${format.videoWidth}x${format.videoHeight} video @ ${fps}fps)`,
|
||||
);
|
||||
)
|
||||
} else {
|
||||
console.log('re-rendering camera page without active camera');
|
||||
console.log('re-rendering camera page without active camera')
|
||||
}
|
||||
|
||||
const frameProcessor = useFrameProcessor((frame) => {
|
||||
'worklet';
|
||||
'worklet'
|
||||
|
||||
console.log(`${frame.timestamp}: ${frame.width}x${frame.height} ${frame.pixelFormat} Frame (${frame.orientation})`);
|
||||
examplePlugin(frame);
|
||||
}, []);
|
||||
console.log(`${frame.timestamp}: ${frame.width}x${frame.height} ${frame.pixelFormat} Frame (${frame.orientation})`)
|
||||
examplePlugin(frame)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -235,7 +235,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -268,4 +268,4 @@ const styles = StyleSheet.create({
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
@@ -1,28 +1,28 @@
|
||||
import { Dimensions, Platform } from 'react-native';
|
||||
import StaticSafeAreaInsets from 'react-native-static-safe-area-insets';
|
||||
import { Dimensions, Platform } from 'react-native'
|
||||
import StaticSafeAreaInsets from 'react-native-static-safe-area-insets'
|
||||
|
||||
export const CONTENT_SPACING = 15;
|
||||
export const CONTENT_SPACING = 15
|
||||
|
||||
const SAFE_BOTTOM =
|
||||
Platform.select({
|
||||
ios: StaticSafeAreaInsets.safeAreaInsetsBottom,
|
||||
}) ?? 0;
|
||||
}) ?? 0
|
||||
|
||||
export const SAFE_AREA_PADDING = {
|
||||
paddingLeft: StaticSafeAreaInsets.safeAreaInsetsLeft + CONTENT_SPACING,
|
||||
paddingTop: StaticSafeAreaInsets.safeAreaInsetsTop + CONTENT_SPACING,
|
||||
paddingRight: StaticSafeAreaInsets.safeAreaInsetsRight + CONTENT_SPACING,
|
||||
paddingBottom: SAFE_BOTTOM + CONTENT_SPACING,
|
||||
};
|
||||
}
|
||||
|
||||
// The maximum zoom _factor_ you should be able to zoom in
|
||||
export const MAX_ZOOM_FACTOR = 20;
|
||||
export const MAX_ZOOM_FACTOR = 20
|
||||
|
||||
export const SCREEN_WIDTH = Dimensions.get('window').width;
|
||||
export const SCREEN_WIDTH = Dimensions.get('window').width
|
||||
export const SCREEN_HEIGHT = Platform.select<number>({
|
||||
android: Dimensions.get('screen').height - StaticSafeAreaInsets.safeAreaInsetsBottom,
|
||||
ios: Dimensions.get('window').height,
|
||||
}) as number;
|
||||
}) as number
|
||||
|
||||
// Capture Button
|
||||
export const CAPTURE_BUTTON_SIZE = 78;
|
||||
export const CAPTURE_BUTTON_SIZE = 78
|
||||
|
@@ -1,82 +1,82 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { StyleSheet, View, ActivityIndicator, PermissionsAndroid, Platform } from 'react-native';
|
||||
import Video, { LoadError, OnLoadData } from 'react-native-video';
|
||||
import { SAFE_AREA_PADDING } from './Constants';
|
||||
import { useIsForeground } from './hooks/useIsForeground';
|
||||
import { PressableOpacity } from 'react-native-pressable-opacity';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
import { Alert } from 'react-native';
|
||||
import { CameraRoll } from '@react-native-camera-roll/camera-roll';
|
||||
import { StatusBarBlurBackground } from './views/StatusBarBlurBackground';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { Routes } from './Routes';
|
||||
import { useIsFocused } from '@react-navigation/core';
|
||||
import FastImage, { OnLoadEvent } from 'react-native-fast-image';
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { StyleSheet, View, ActivityIndicator, PermissionsAndroid, Platform } from 'react-native'
|
||||
import Video, { LoadError, OnLoadData } from 'react-native-video'
|
||||
import { SAFE_AREA_PADDING } from './Constants'
|
||||
import { useIsForeground } from './hooks/useIsForeground'
|
||||
import { PressableOpacity } from 'react-native-pressable-opacity'
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons'
|
||||
import { Alert } from 'react-native'
|
||||
import { CameraRoll } from '@react-native-camera-roll/camera-roll'
|
||||
import { StatusBarBlurBackground } from './views/StatusBarBlurBackground'
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
|
||||
import type { Routes } from './Routes'
|
||||
import { useIsFocused } from '@react-navigation/core'
|
||||
import FastImage, { OnLoadEvent } from 'react-native-fast-image'
|
||||
|
||||
const requestSavePermission = async (): Promise<boolean> => {
|
||||
if (Platform.OS !== 'android') return true;
|
||||
if (Platform.OS !== 'android') return true
|
||||
|
||||
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;
|
||||
if (permission == null) return false;
|
||||
let hasPermission = await PermissionsAndroid.check(permission);
|
||||
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
|
||||
if (permission == null) return false
|
||||
let hasPermission = await PermissionsAndroid.check(permission)
|
||||
if (!hasPermission) {
|
||||
const permissionRequestResult = await PermissionsAndroid.request(permission);
|
||||
hasPermission = permissionRequestResult === 'granted';
|
||||
const permissionRequestResult = await PermissionsAndroid.request(permission)
|
||||
hasPermission = permissionRequestResult === 'granted'
|
||||
}
|
||||
return hasPermission;
|
||||
};
|
||||
return hasPermission
|
||||
}
|
||||
|
||||
const isVideoOnLoadEvent = (event: OnLoadData | OnLoadEvent): event is OnLoadData => 'duration' in event && 'naturalSize' in event;
|
||||
const isVideoOnLoadEvent = (event: OnLoadData | OnLoadEvent): event is OnLoadData => 'duration' in event && 'naturalSize' in event
|
||||
|
||||
type Props = NativeStackScreenProps<Routes, 'MediaPage'>;
|
||||
type Props = NativeStackScreenProps<Routes, 'MediaPage'>
|
||||
export function MediaPage({ navigation, route }: Props): React.ReactElement {
|
||||
const { path, type } = route.params;
|
||||
const [hasMediaLoaded, setHasMediaLoaded] = useState(false);
|
||||
const isForeground = useIsForeground();
|
||||
const isScreenFocused = useIsFocused();
|
||||
const isVideoPaused = !isForeground || !isScreenFocused;
|
||||
const [savingState, setSavingState] = useState<'none' | 'saving' | 'saved'>('none');
|
||||
const { path, type } = route.params
|
||||
const [hasMediaLoaded, setHasMediaLoaded] = useState(false)
|
||||
const isForeground = useIsForeground()
|
||||
const isScreenFocused = useIsFocused()
|
||||
const isVideoPaused = !isForeground || !isScreenFocused
|
||||
const [savingState, setSavingState] = useState<'none' | 'saving' | 'saved'>('none')
|
||||
|
||||
const onMediaLoad = useCallback((event: OnLoadData | OnLoadEvent) => {
|
||||
if (isVideoOnLoadEvent(event)) {
|
||||
console.log(
|
||||
`Video loaded. Size: ${event.naturalSize.width}x${event.naturalSize.height} (${event.naturalSize.orientation}, ${event.duration} seconds)`,
|
||||
);
|
||||
)
|
||||
} else {
|
||||
console.log(`Image loaded. Size: ${event.nativeEvent.width}x${event.nativeEvent.height}`);
|
||||
console.log(`Image loaded. Size: ${event.nativeEvent.width}x${event.nativeEvent.height}`)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
const onMediaLoadEnd = useCallback(() => {
|
||||
console.log('media has loaded.');
|
||||
setHasMediaLoaded(true);
|
||||
}, []);
|
||||
console.log('media has loaded.')
|
||||
setHasMediaLoaded(true)
|
||||
}, [])
|
||||
const onMediaLoadError = useCallback((error: LoadError) => {
|
||||
console.log(`failed to load media: ${JSON.stringify(error)}`);
|
||||
}, []);
|
||||
console.log(`failed to load media: ${JSON.stringify(error)}`)
|
||||
}, [])
|
||||
|
||||
const onSavePressed = useCallback(async () => {
|
||||
try {
|
||||
setSavingState('saving');
|
||||
setSavingState('saving')
|
||||
|
||||
const hasPermission = await requestSavePermission();
|
||||
const hasPermission = await requestSavePermission()
|
||||
if (!hasPermission) {
|
||||
Alert.alert('Permission denied!', 'Vision Camera does not have permission to save the media to your camera roll.');
|
||||
return;
|
||||
Alert.alert('Permission denied!', 'Vision Camera does not have permission to save the media to your camera roll.')
|
||||
return
|
||||
}
|
||||
await CameraRoll.save(`file://${path}`, {
|
||||
type: type,
|
||||
});
|
||||
setSavingState('saved');
|
||||
})
|
||||
setSavingState('saved')
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : JSON.stringify(e);
|
||||
setSavingState('none');
|
||||
Alert.alert('Failed to save!', `An unexpected error occured while trying to save your ${type}. ${message}`);
|
||||
const message = e instanceof Error ? e.message : JSON.stringify(e)
|
||||
setSavingState('none')
|
||||
Alert.alert('Failed to save!', `An unexpected error occured while trying to save your ${type}. ${message}`)
|
||||
}
|
||||
}, [path, type]);
|
||||
}, [path, type])
|
||||
|
||||
const source = useMemo(() => ({ uri: `file://${path}` }), [path]);
|
||||
const source = useMemo(() => ({ uri: `file://${path}` }), [path])
|
||||
|
||||
const screenStyle = useMemo(() => ({ opacity: hasMediaLoaded ? 1 : 0 }), [hasMediaLoaded]);
|
||||
const screenStyle = useMemo(() => ({ opacity: hasMediaLoaded ? 1 : 0 }), [hasMediaLoaded])
|
||||
|
||||
return (
|
||||
<View style={[styles.container, screenStyle]}>
|
||||
@@ -116,7 +116,7 @@ export function MediaPage({ navigation, route }: Props): React.ReactElement {
|
||||
|
||||
<StatusBarBlurBackground />
|
||||
</View>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -148,4 +148,4 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
textShadowRadius: 1,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
@@ -1,41 +1,41 @@
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { ImageRequireSource, Linking } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { ImageRequireSource, Linking } from 'react-native'
|
||||
|
||||
import { StyleSheet, View, Text, Image } from 'react-native';
|
||||
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera';
|
||||
import { CONTENT_SPACING, SAFE_AREA_PADDING } from './Constants';
|
||||
import type { Routes } from './Routes';
|
||||
import { StyleSheet, View, Text, Image } from 'react-native'
|
||||
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera'
|
||||
import { CONTENT_SPACING, SAFE_AREA_PADDING } from './Constants'
|
||||
import type { Routes } from './Routes'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const BANNER_IMAGE = require('./img/11.png') as ImageRequireSource;
|
||||
const BANNER_IMAGE = require('./img/11.png') as ImageRequireSource
|
||||
|
||||
type Props = NativeStackScreenProps<Routes, 'PermissionsPage'>;
|
||||
type Props = NativeStackScreenProps<Routes, 'PermissionsPage'>
|
||||
export function PermissionsPage({ navigation }: Props): React.ReactElement {
|
||||
const [cameraPermissionStatus, setCameraPermissionStatus] = useState<CameraPermissionStatus>('not-determined');
|
||||
const [microphonePermissionStatus, setMicrophonePermissionStatus] = useState<CameraPermissionStatus>('not-determined');
|
||||
const [cameraPermissionStatus, setCameraPermissionStatus] = useState<CameraPermissionStatus>('not-determined')
|
||||
const [microphonePermissionStatus, setMicrophonePermissionStatus] = useState<CameraPermissionStatus>('not-determined')
|
||||
|
||||
const requestMicrophonePermission = useCallback(async () => {
|
||||
console.log('Requesting microphone permission...');
|
||||
const permission = await Camera.requestMicrophonePermission();
|
||||
console.log(`Microphone permission status: ${permission}`);
|
||||
console.log('Requesting microphone permission...')
|
||||
const permission = await Camera.requestMicrophonePermission()
|
||||
console.log(`Microphone permission status: ${permission}`)
|
||||
|
||||
if (permission === 'denied') await Linking.openSettings();
|
||||
setMicrophonePermissionStatus(permission);
|
||||
}, []);
|
||||
if (permission === 'denied') await Linking.openSettings()
|
||||
setMicrophonePermissionStatus(permission)
|
||||
}, [])
|
||||
|
||||
const requestCameraPermission = useCallback(async () => {
|
||||
console.log('Requesting camera permission...');
|
||||
const permission = await Camera.requestCameraPermission();
|
||||
console.log(`Camera permission status: ${permission}`);
|
||||
console.log('Requesting camera permission...')
|
||||
const permission = await Camera.requestCameraPermission()
|
||||
console.log(`Camera permission status: ${permission}`)
|
||||
|
||||
if (permission === 'denied') await Linking.openSettings();
|
||||
setCameraPermissionStatus(permission);
|
||||
}, []);
|
||||
if (permission === 'denied') await Linking.openSettings()
|
||||
setCameraPermissionStatus(permission)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (cameraPermissionStatus === 'granted' && microphonePermissionStatus === 'granted') navigation.replace('CameraPage');
|
||||
}, [cameraPermissionStatus, microphonePermissionStatus, navigation]);
|
||||
if (cameraPermissionStatus === 'granted' && microphonePermissionStatus === 'granted') navigation.replace('CameraPage')
|
||||
}, [cameraPermissionStatus, microphonePermissionStatus, navigation])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -60,7 +60,7 @@ export function PermissionsPage({ navigation }: Props): React.ReactElement {
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -93,4 +93,4 @@ const styles = StyleSheet.create({
|
||||
bold: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
@@ -1,8 +1,8 @@
|
||||
export type Routes = {
|
||||
PermissionsPage: undefined;
|
||||
CameraPage: undefined;
|
||||
PermissionsPage: undefined
|
||||
CameraPage: undefined
|
||||
MediaPage: {
|
||||
path: string;
|
||||
type: 'video' | 'photo';
|
||||
};
|
||||
};
|
||||
path: string
|
||||
type: 'video' | 'photo'
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { VisionCameraProxy, Frame } from 'react-native-vision-camera';
|
||||
import { VisionCameraProxy, Frame } from 'react-native-vision-camera'
|
||||
|
||||
const plugin = VisionCameraProxy.getFrameProcessorPlugin('example_plugin');
|
||||
const plugin = VisionCameraProxy.getFrameProcessorPlugin('example_plugin')
|
||||
|
||||
export function examplePlugin(frame: Frame): string[] {
|
||||
'worklet';
|
||||
'worklet'
|
||||
|
||||
if (plugin == null) throw new Error('Failed to load Frame Processor Plugin "example_plugin"!');
|
||||
if (plugin == null) throw new Error('Failed to load Frame Processor Plugin "example_plugin"!')
|
||||
|
||||
return plugin.call(frame, {
|
||||
someString: 'hello!',
|
||||
@@ -13,5 +13,5 @@ export function examplePlugin(frame: Frame): string[] {
|
||||
someNumber: 42,
|
||||
someObject: { test: 0, second: 'test' },
|
||||
someArray: ['another test', 5],
|
||||
}) as string[];
|
||||
}) as string[]
|
||||
}
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { AppState, AppStateStatus } from 'react-native';
|
||||
import { useState } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { AppState, AppStateStatus } from 'react-native'
|
||||
|
||||
export const useIsForeground = (): boolean => {
|
||||
const [isForeground, setIsForeground] = useState(true);
|
||||
const [isForeground, setIsForeground] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (state: AppStateStatus): void => {
|
||||
setIsForeground(state === 'active');
|
||||
};
|
||||
const listener = AppState.addEventListener('change', onChange);
|
||||
return () => listener.remove();
|
||||
}, [setIsForeground]);
|
||||
setIsForeground(state === 'active')
|
||||
}
|
||||
const listener = AppState.addEventListener('change', onChange)
|
||||
return () => listener.remove()
|
||||
}, [setIsForeground])
|
||||
|
||||
return isForeground;
|
||||
};
|
||||
return isForeground
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { StyleSheet, View, ViewProps } from 'react-native';
|
||||
import React, { useCallback, useMemo, useRef } from 'react'
|
||||
import { StyleSheet, View, ViewProps } from 'react-native'
|
||||
import {
|
||||
PanGestureHandler,
|
||||
PanGestureHandlerGestureEvent,
|
||||
State,
|
||||
TapGestureHandler,
|
||||
TapGestureHandlerStateChangeEvent,
|
||||
} from 'react-native-gesture-handler';
|
||||
} from 'react-native-gesture-handler'
|
||||
import Reanimated, {
|
||||
cancelAnimation,
|
||||
Easing,
|
||||
@@ -18,29 +18,29 @@ import Reanimated, {
|
||||
useAnimatedGestureHandler,
|
||||
useSharedValue,
|
||||
withRepeat,
|
||||
} from 'react-native-reanimated';
|
||||
import type { Camera, PhotoFile, TakePhotoOptions, VideoFile } from 'react-native-vision-camera';
|
||||
import { CAPTURE_BUTTON_SIZE, SCREEN_HEIGHT, SCREEN_WIDTH } from './../Constants';
|
||||
} from 'react-native-reanimated'
|
||||
import type { Camera, PhotoFile, TakePhotoOptions, VideoFile } from 'react-native-vision-camera'
|
||||
import { CAPTURE_BUTTON_SIZE, SCREEN_HEIGHT, SCREEN_WIDTH } from './../Constants'
|
||||
|
||||
const PAN_GESTURE_HANDLER_FAIL_X = [-SCREEN_WIDTH, SCREEN_WIDTH];
|
||||
const PAN_GESTURE_HANDLER_ACTIVE_Y = [-2, 2];
|
||||
const PAN_GESTURE_HANDLER_FAIL_X = [-SCREEN_WIDTH, SCREEN_WIDTH]
|
||||
const PAN_GESTURE_HANDLER_ACTIVE_Y = [-2, 2]
|
||||
|
||||
const START_RECORDING_DELAY = 200;
|
||||
const BORDER_WIDTH = CAPTURE_BUTTON_SIZE * 0.1;
|
||||
const START_RECORDING_DELAY = 200
|
||||
const BORDER_WIDTH = CAPTURE_BUTTON_SIZE * 0.1
|
||||
|
||||
interface Props extends ViewProps {
|
||||
camera: React.RefObject<Camera>;
|
||||
onMediaCaptured: (media: PhotoFile | VideoFile, type: 'photo' | 'video') => void;
|
||||
camera: React.RefObject<Camera>
|
||||
onMediaCaptured: (media: PhotoFile | VideoFile, type: 'photo' | 'video') => void
|
||||
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
cameraZoom: Reanimated.SharedValue<number>;
|
||||
minZoom: number
|
||||
maxZoom: number
|
||||
cameraZoom: Reanimated.SharedValue<number>
|
||||
|
||||
flash: 'off' | 'on';
|
||||
flash: 'off' | 'on'
|
||||
|
||||
enabled: boolean;
|
||||
enabled: boolean
|
||||
|
||||
setIsPressingButton: (isPressingButton: boolean) => void;
|
||||
setIsPressingButton: (isPressingButton: boolean) => void
|
||||
}
|
||||
|
||||
const _CaptureButton: React.FC<Props> = ({
|
||||
@@ -55,9 +55,9 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
style,
|
||||
...props
|
||||
}): React.ReactElement => {
|
||||
const pressDownDate = useRef<Date | undefined>(undefined);
|
||||
const isRecording = useRef(false);
|
||||
const recordingProgress = useSharedValue(0);
|
||||
const pressDownDate = useRef<Date | undefined>(undefined)
|
||||
const isRecording = useRef(false)
|
||||
const recordingProgress = useSharedValue(0)
|
||||
const takePhotoOptions = useMemo<TakePhotoOptions>(
|
||||
() => ({
|
||||
photoCodec: 'jpeg',
|
||||
@@ -67,66 +67,66 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
enableShutterSound: false,
|
||||
}),
|
||||
[flash],
|
||||
);
|
||||
const isPressingButton = useSharedValue(false);
|
||||
)
|
||||
const isPressingButton = useSharedValue(false)
|
||||
|
||||
//#region Camera Capture
|
||||
const takePhoto = useCallback(async () => {
|
||||
try {
|
||||
if (camera.current == null) throw new Error('Camera ref is null!');
|
||||
if (camera.current == null) throw new Error('Camera ref is null!')
|
||||
|
||||
console.log('Taking photo...');
|
||||
const photo = await camera.current.takePhoto(takePhotoOptions);
|
||||
onMediaCaptured(photo, 'photo');
|
||||
console.log('Taking photo...')
|
||||
const photo = await camera.current.takePhoto(takePhotoOptions)
|
||||
onMediaCaptured(photo, 'photo')
|
||||
} catch (e) {
|
||||
console.error('Failed to take photo!', e);
|
||||
console.error('Failed to take photo!', e)
|
||||
}
|
||||
}, [camera, onMediaCaptured, takePhotoOptions]);
|
||||
}, [camera, onMediaCaptured, takePhotoOptions])
|
||||
|
||||
const onStoppedRecording = useCallback(() => {
|
||||
isRecording.current = false;
|
||||
cancelAnimation(recordingProgress);
|
||||
console.log('stopped recording video!');
|
||||
}, [recordingProgress]);
|
||||
isRecording.current = false
|
||||
cancelAnimation(recordingProgress)
|
||||
console.log('stopped recording video!')
|
||||
}, [recordingProgress])
|
||||
const stopRecording = useCallback(async () => {
|
||||
try {
|
||||
if (camera.current == null) throw new Error('Camera ref is null!');
|
||||
if (camera.current == null) throw new Error('Camera ref is null!')
|
||||
|
||||
console.log('calling stopRecording()...');
|
||||
await camera.current.stopRecording();
|
||||
console.log('called stopRecording()!');
|
||||
console.log('calling stopRecording()...')
|
||||
await camera.current.stopRecording()
|
||||
console.log('called stopRecording()!')
|
||||
} catch (e) {
|
||||
console.error('failed to stop recording!', e);
|
||||
console.error('failed to stop recording!', e)
|
||||
}
|
||||
}, [camera]);
|
||||
}, [camera])
|
||||
const startRecording = useCallback(() => {
|
||||
try {
|
||||
if (camera.current == null) throw new Error('Camera ref is null!');
|
||||
if (camera.current == null) throw new Error('Camera ref is null!')
|
||||
|
||||
console.log('calling startRecording()...');
|
||||
console.log('calling startRecording()...')
|
||||
camera.current.startRecording({
|
||||
flash: flash,
|
||||
onRecordingError: (error) => {
|
||||
console.error('Recording failed!', error);
|
||||
onStoppedRecording();
|
||||
console.error('Recording failed!', error)
|
||||
onStoppedRecording()
|
||||
},
|
||||
onRecordingFinished: (video) => {
|
||||
console.log(`Recording successfully finished! ${video.path}`);
|
||||
onMediaCaptured(video, 'video');
|
||||
onStoppedRecording();
|
||||
console.log(`Recording successfully finished! ${video.path}`)
|
||||
onMediaCaptured(video, 'video')
|
||||
onStoppedRecording()
|
||||
},
|
||||
});
|
||||
})
|
||||
// TODO: wait until startRecording returns to actually find out if the recording has successfully started
|
||||
console.log('called startRecording()!');
|
||||
isRecording.current = true;
|
||||
console.log('called startRecording()!')
|
||||
isRecording.current = true
|
||||
} catch (e) {
|
||||
console.error('failed to start recording!', e, 'camera');
|
||||
console.error('failed to start recording!', e, 'camera')
|
||||
}
|
||||
}, [camera, flash, onMediaCaptured, onStoppedRecording]);
|
||||
}, [camera, flash, onMediaCaptured, onStoppedRecording])
|
||||
//#endregion
|
||||
|
||||
//#region Tap handler
|
||||
const tapHandler = useRef<TapGestureHandler>();
|
||||
const tapHandler = useRef<TapGestureHandler>()
|
||||
const onHandlerStateChanged = useCallback(
|
||||
async ({ nativeEvent: event }: TapGestureHandlerStateChangeEvent) => {
|
||||
// This is the gesture handler for the circular "shutter" button.
|
||||
@@ -138,73 +138,73 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
// if `pressDownDate` was less than 200ms ago, we know that the intention of the user is to take a photo. We check the `takePhotoPromise` if
|
||||
// there already is an ongoing (or already resolved) takePhoto() call (remember that we called takePhoto() when the user pressed down), and
|
||||
// if yes, use that. If no, we just try calling takePhoto() again
|
||||
console.debug(`state: ${Object.keys(State)[event.state]}`);
|
||||
console.debug(`state: ${Object.keys(State)[event.state]}`)
|
||||
switch (event.state) {
|
||||
case State.BEGAN: {
|
||||
// enter "recording mode"
|
||||
recordingProgress.value = 0;
|
||||
isPressingButton.value = true;
|
||||
const now = new Date();
|
||||
pressDownDate.current = now;
|
||||
recordingProgress.value = 0
|
||||
isPressingButton.value = true
|
||||
const now = new Date()
|
||||
pressDownDate.current = now
|
||||
setTimeout(() => {
|
||||
if (pressDownDate.current === now) {
|
||||
// user is still pressing down after 200ms, so his intention is to create a video
|
||||
startRecording();
|
||||
startRecording()
|
||||
}
|
||||
}, START_RECORDING_DELAY);
|
||||
setIsPressingButton(true);
|
||||
return;
|
||||
}, START_RECORDING_DELAY)
|
||||
setIsPressingButton(true)
|
||||
return
|
||||
}
|
||||
case State.END:
|
||||
case State.FAILED:
|
||||
case State.CANCELLED: {
|
||||
// exit "recording mode"
|
||||
try {
|
||||
if (pressDownDate.current == null) throw new Error('PressDownDate ref .current was null!');
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - pressDownDate.current.getTime();
|
||||
pressDownDate.current = undefined;
|
||||
if (pressDownDate.current == null) throw new Error('PressDownDate ref .current was null!')
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - pressDownDate.current.getTime()
|
||||
pressDownDate.current = undefined
|
||||
if (diff < START_RECORDING_DELAY) {
|
||||
// user has released the button within 200ms, so his intention is to take a single picture.
|
||||
await takePhoto();
|
||||
await takePhoto()
|
||||
} else {
|
||||
// user has held the button for more than 200ms, so he has been recording this entire time.
|
||||
await stopRecording();
|
||||
await stopRecording()
|
||||
}
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
isPressingButton.value = false;
|
||||
setIsPressingButton(false);
|
||||
}, 500);
|
||||
isPressingButton.value = false
|
||||
setIsPressingButton(false)
|
||||
}, 500)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
default:
|
||||
break;
|
||||
break
|
||||
}
|
||||
},
|
||||
[isPressingButton, recordingProgress, setIsPressingButton, startRecording, stopRecording, takePhoto],
|
||||
);
|
||||
)
|
||||
//#endregion
|
||||
//#region Pan handler
|
||||
const panHandler = useRef<PanGestureHandler>();
|
||||
const panHandler = useRef<PanGestureHandler>()
|
||||
const onPanGestureEvent = useAnimatedGestureHandler<PanGestureHandlerGestureEvent, { offsetY?: number; startY?: number }>({
|
||||
onStart: (event, context) => {
|
||||
context.startY = event.absoluteY;
|
||||
const yForFullZoom = context.startY * 0.7;
|
||||
const offsetYForFullZoom = context.startY - yForFullZoom;
|
||||
context.startY = event.absoluteY
|
||||
const yForFullZoom = context.startY * 0.7
|
||||
const offsetYForFullZoom = context.startY - yForFullZoom
|
||||
|
||||
// extrapolate [0 ... 1] zoom -> [0 ... Y_FOR_FULL_ZOOM] finger position
|
||||
context.offsetY = interpolate(cameraZoom.value, [minZoom, maxZoom], [0, offsetYForFullZoom], Extrapolate.CLAMP);
|
||||
context.offsetY = interpolate(cameraZoom.value, [minZoom, maxZoom], [0, offsetYForFullZoom], Extrapolate.CLAMP)
|
||||
},
|
||||
onActive: (event, context) => {
|
||||
const offset = context.offsetY ?? 0;
|
||||
const startY = context.startY ?? SCREEN_HEIGHT;
|
||||
const yForFullZoom = startY * 0.7;
|
||||
const offset = context.offsetY ?? 0
|
||||
const startY = context.startY ?? SCREEN_HEIGHT
|
||||
const yForFullZoom = startY * 0.7
|
||||
|
||||
cameraZoom.value = interpolate(event.absoluteY - offset, [yForFullZoom, startY], [maxZoom, minZoom], Extrapolate.CLAMP);
|
||||
cameraZoom.value = interpolate(event.absoluteY - offset, [yForFullZoom, startY], [maxZoom, minZoom], Extrapolate.CLAMP)
|
||||
},
|
||||
});
|
||||
})
|
||||
//#endregion
|
||||
|
||||
const shadowStyle = useAnimatedStyle(
|
||||
@@ -220,9 +220,9 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
],
|
||||
}),
|
||||
[isPressingButton],
|
||||
);
|
||||
)
|
||||
const buttonStyle = useAnimatedStyle(() => {
|
||||
let scale: number;
|
||||
let scale: number
|
||||
if (enabled) {
|
||||
if (isPressingButton.value) {
|
||||
scale = withRepeat(
|
||||
@@ -232,18 +232,18 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
}),
|
||||
-1,
|
||||
true,
|
||||
);
|
||||
)
|
||||
} else {
|
||||
scale = withSpring(0.9, {
|
||||
stiffness: 500,
|
||||
damping: 300,
|
||||
});
|
||||
})
|
||||
}
|
||||
} else {
|
||||
scale = withSpring(0.6, {
|
||||
stiffness: 500,
|
||||
damping: 300,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -256,8 +256,8 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
scale: scale,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [enabled, isPressingButton]);
|
||||
}
|
||||
}, [enabled, isPressingButton])
|
||||
|
||||
return (
|
||||
<TapGestureHandler
|
||||
@@ -282,10 +282,10 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
</PanGestureHandler>
|
||||
</Reanimated.View>
|
||||
</TapGestureHandler>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const CaptureButton = React.memo(_CaptureButton);
|
||||
export const CaptureButton = React.memo(_CaptureButton)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
@@ -305,4 +305,4 @@ const styles = StyleSheet.create({
|
||||
borderWidth: BORDER_WIDTH,
|
||||
borderColor: 'white',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { BlurView, BlurViewProps } from '@react-native-community/blur';
|
||||
import React from 'react';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
import StaticSafeAreaInsets from 'react-native-static-safe-area-insets';
|
||||
import { BlurView, BlurViewProps } from '@react-native-community/blur'
|
||||
import React from 'react'
|
||||
import { Platform, StyleSheet } from 'react-native'
|
||||
import StaticSafeAreaInsets from 'react-native-static-safe-area-insets'
|
||||
|
||||
const FALLBACK_COLOR = 'rgba(140, 140, 140, 0.3)';
|
||||
const FALLBACK_COLOR = 'rgba(140, 140, 140, 0.3)'
|
||||
|
||||
const StatusBarBlurBackgroundImpl = ({ style, ...props }: BlurViewProps): React.ReactElement | null => {
|
||||
if (Platform.OS !== 'ios') return null;
|
||||
if (Platform.OS !== 'ios') return null
|
||||
|
||||
return (
|
||||
<BlurView
|
||||
@@ -16,10 +16,10 @@ const StatusBarBlurBackgroundImpl = ({ style, ...props }: BlurViewProps): React.
|
||||
reducedTransparencyFallbackColor={FALLBACK_COLOR}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export const StatusBarBlurBackground = React.memo(StatusBarBlurBackgroundImpl);
|
||||
export const StatusBarBlurBackground = React.memo(StatusBarBlurBackgroundImpl)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
statusBarBackground: {
|
||||
@@ -29,4 +29,4 @@ const styles = StyleSheet.create({
|
||||
right: 0,
|
||||
height: StaticSafeAreaInsets.safeAreaInsetsTop,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
Reference in New Issue
Block a user