ESLint autofix
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import { Navigation } from "react-native-navigation";
|
||||
import { Navigation } from 'react-native-navigation';
|
||||
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
|
||||
import { App } from './src/App';
|
||||
import { Settings } from './src/Settings';
|
||||
@@ -16,14 +16,14 @@ Navigation.setDefaultOptions({
|
||||
},
|
||||
layout: {
|
||||
backgroundColor: 'black',
|
||||
componentBackgroundColor: 'black'
|
||||
componentBackgroundColor: 'black',
|
||||
},
|
||||
statusBar: {
|
||||
animated: true,
|
||||
drawBehind: true,
|
||||
translucent: true,
|
||||
visible: true,
|
||||
style: 'dark'
|
||||
style: 'dark',
|
||||
},
|
||||
animations: {
|
||||
setRoot: {
|
||||
@@ -31,31 +31,40 @@ Navigation.setDefaultOptions({
|
||||
duration: 500,
|
||||
from: 0,
|
||||
to: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Navigation.registerComponent('Splash', () => gestureHandlerRootHOC(Splash), () => Splash);
|
||||
Navigation.registerComponent('Home', () => gestureHandlerRootHOC(App), () => App);
|
||||
Navigation.registerComponent('Media', () => gestureHandlerRootHOC(Media), () => Media);
|
||||
Navigation.registerComponent('Settings', () => gestureHandlerRootHOC(Settings), () => Settings);
|
||||
Navigation.registerComponent(
|
||||
'Splash',
|
||||
() => gestureHandlerRootHOC(Splash),
|
||||
() => Splash,
|
||||
);
|
||||
Navigation.registerComponent(
|
||||
'Home',
|
||||
() => gestureHandlerRootHOC(App),
|
||||
() => App,
|
||||
);
|
||||
Navigation.registerComponent(
|
||||
'Media',
|
||||
() => gestureHandlerRootHOC(Media),
|
||||
() => Media,
|
||||
);
|
||||
Navigation.registerComponent(
|
||||
'Settings',
|
||||
() => gestureHandlerRootHOC(Settings),
|
||||
() => Settings,
|
||||
);
|
||||
|
||||
Navigation.events().registerNavigationButtonPressedListener((event) => {
|
||||
if (event.buttonId === "back") {
|
||||
Navigation.pop(event.componentId);
|
||||
}
|
||||
if (event.buttonId === 'back') Navigation.pop(event.componentId);
|
||||
});
|
||||
|
||||
Navigation.events().registerAppLaunchedListener(async () => {
|
||||
const [cameraPermission, microphonePermission] = await Promise.all([
|
||||
Camera.getCameraPermissionStatus(),
|
||||
Camera.getMicrophonePermissionStatus(),
|
||||
]);
|
||||
let rootName = "Splash";
|
||||
if (cameraPermission === "authorized" && microphonePermission === "authorized") {
|
||||
rootName = "Home";
|
||||
}
|
||||
const [cameraPermission, microphonePermission] = await Promise.all([Camera.getCameraPermissionStatus(), Camera.getMicrophonePermissionStatus()]);
|
||||
let rootName = 'Splash';
|
||||
if (cameraPermission === 'authorized' && microphonePermission === 'authorized') rootName = 'Home';
|
||||
|
||||
Navigation.setRoot({
|
||||
root: {
|
||||
@@ -63,11 +72,11 @@ Navigation.events().registerAppLaunchedListener(async () => {
|
||||
children: [
|
||||
{
|
||||
component: {
|
||||
name: rootName
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
name: rootName,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@@ -16,12 +16,7 @@ module.exports = {
|
||||
// We need to make sure that only one version is loaded for peerDependencies
|
||||
// So we blacklist them at the root, and alias them to the versions in example's node_modules
|
||||
resolver: {
|
||||
blacklistRE: blacklist(
|
||||
modules.map(
|
||||
(m) =>
|
||||
new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`)
|
||||
)
|
||||
),
|
||||
blacklistRE: blacklist(modules.map((m) => new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`))),
|
||||
|
||||
extraNodeModules: modules.reduce((acc, name) => {
|
||||
acc[name] = path.join(__dirname, 'node_modules', name);
|
||||
|
@@ -1,7 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { useRef, useState, useMemo, useCallback } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { PinchGestureHandler, PinchGestureHandlerGestureEvent, State, TapGestureHandler, TapGestureHandlerStateChangeEvent } from 'react-native-gesture-handler';
|
||||
import {
|
||||
PinchGestureHandler,
|
||||
PinchGestureHandlerGestureEvent,
|
||||
State,
|
||||
TapGestureHandler,
|
||||
TapGestureHandlerStateChangeEvent,
|
||||
} from 'react-native-gesture-handler';
|
||||
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
||||
import type { CameraDevice, CameraDeviceFormat, CameraProps, CameraRuntimeError, PhotoFile, VideoFile } from 'react-native-vision-camera';
|
||||
import { Camera } from 'react-native-vision-camera';
|
||||
@@ -37,23 +43,15 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
const isForeground = useIsForeground();
|
||||
const isActive = isFocussed && isForeground;
|
||||
|
||||
const [cameraPosition, setCameraPosition] = useState<"front" | "back">(
|
||||
"back"
|
||||
);
|
||||
const [cameraPosition, setCameraPosition] = useState<'front' | 'back'>('back');
|
||||
const [enableHdr, setEnableHdr] = useState(false);
|
||||
const [flash, setFlash] = useState<"off" | "on">("off");
|
||||
const [flash, setFlash] = useState<'off' | 'on'>('off');
|
||||
const [enableNightMode, setEnableNightMode] = useState(false);
|
||||
|
||||
// camera format settings
|
||||
const [devices, setDevices] = useState<CameraDevice[]>([]); // All available camera devices, sorted by "best device" (descending)
|
||||
const device = useMemo<CameraDevice | undefined>(
|
||||
() => devices.find((d) => d.position === cameraPosition),
|
||||
[cameraPosition, devices]
|
||||
);
|
||||
const formats = useMemo<CameraDeviceFormat[]>(
|
||||
() => device?.formats.sort(compareFormats) ?? [],
|
||||
[device?.formats]
|
||||
);
|
||||
const device = useMemo<CameraDevice | undefined>(() => devices.find((d) => d.position === cameraPosition), [cameraPosition, devices]);
|
||||
const formats = useMemo<CameraDeviceFormat[]>(() => device?.formats.sort(compareFormats) ?? [], [device?.formats]);
|
||||
|
||||
//#region Memos
|
||||
const fps = useMemo(() => {
|
||||
@@ -62,37 +60,24 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
return 30;
|
||||
}
|
||||
|
||||
const supportsHdrAtHighFps = formats.some(
|
||||
(f) =>
|
||||
f.supportsVideoHDR &&
|
||||
f.frameRateRanges.some((r) => frameRateIncluded(r, HIGH_FPS))
|
||||
);
|
||||
const supportsHdrAtHighFps = formats.some((f) => f.supportsVideoHDR && f.frameRateRanges.some((r) => frameRateIncluded(r, HIGH_FPS)));
|
||||
if (enableHdr && !supportsHdrAtHighFps) {
|
||||
// User has enabled HDR, but HDR is not supported at HIGH_FPS.
|
||||
return 30;
|
||||
}
|
||||
|
||||
const supportsHighFps = formats.some((f) =>
|
||||
f.frameRateRanges.some((r) => frameRateIncluded(r, HIGH_FPS))
|
||||
);
|
||||
const supportsHighFps = formats.some((f) => f.frameRateRanges.some((r) => frameRateIncluded(r, HIGH_FPS)));
|
||||
if (!supportsHighFps) {
|
||||
// HIGH_FPS is not supported by any format.
|
||||
return 30;
|
||||
}
|
||||
// If nothing blocks us from using it, we default to HIGH_FPS.
|
||||
return HIGH_FPS;
|
||||
}, [device, enableHdr, enableNightMode, formats,]);
|
||||
}, [device, enableHdr, enableNightMode, formats]);
|
||||
|
||||
const supportsCameraFlipping = useMemo(
|
||||
() =>
|
||||
devices.some((d) => d.position === "back") &&
|
||||
devices.some((d) => d.position === "front"),
|
||||
[devices]
|
||||
);
|
||||
const supportsCameraFlipping = useMemo(() => devices.some((d) => d.position === 'back') && devices.some((d) => d.position === 'front'), [devices]);
|
||||
const supportsFlash = device?.hasFlash ?? false;
|
||||
const supportsHdr = useMemo(() => formats.some((f) => f.supportsVideoHDR), [
|
||||
formats,
|
||||
]);
|
||||
const supportsHdr = useMemo(() => formats.some((f) => f.supportsVideoHDR), [formats]);
|
||||
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
|
||||
@@ -118,14 +103,9 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
|
||||
const cameraAnimatedProps = useAnimatedProps<Partial<CameraProps>>(
|
||||
() => ({
|
||||
zoom: interpolate(
|
||||
zoom.value,
|
||||
[0, neutralZoomScaled, 1],
|
||||
[0, neutralZoom, maxZoomScaled],
|
||||
Extrapolate.CLAMP
|
||||
),
|
||||
zoom: interpolate(zoom.value, [0, neutralZoomScaled, 1], [0, neutralZoom, maxZoomScaled], Extrapolate.CLAMP),
|
||||
}),
|
||||
[maxZoomScaled, neutralZoom, neutralZoomScaled, zoom]
|
||||
[maxZoomScaled, neutralZoom, neutralZoomScaled, zoom],
|
||||
);
|
||||
//#endregion
|
||||
|
||||
@@ -134,45 +114,42 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
(_isPressingButton: boolean) => {
|
||||
isPressingButton.value = _isPressingButton;
|
||||
},
|
||||
[isPressingButton]
|
||||
[isPressingButton],
|
||||
);
|
||||
// Camera callbacks
|
||||
const onError = useCallback((error: CameraRuntimeError) => {
|
||||
console.error(error);
|
||||
}, []);
|
||||
const onInitialized = useCallback(() => {
|
||||
console.log(`Camera initialized!`);
|
||||
console.log('Camera initialized!');
|
||||
setIsCameraInitialized(true);
|
||||
}, []);
|
||||
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,
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
[]
|
||||
);
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
const onFlipCameraPressed = useCallback(() => {
|
||||
setCameraPosition((p) => (p === "back" ? "front" : "back"));
|
||||
setCameraPosition((p) => (p === 'back' ? 'front' : 'back'));
|
||||
}, []);
|
||||
const onHdrSwitchPressed = useCallback(() => {
|
||||
setEnableHdr((h) => !h);
|
||||
}, []);
|
||||
const onFlashPressed = useCallback(() => {
|
||||
setFlash((f) => (f === "off" ? "on" : "off"));
|
||||
setFlash((f) => (f === 'off' ? 'on' : 'off'));
|
||||
}, []);
|
||||
const onNightModePressed = useCallback(() => {
|
||||
setEnableNightMode((n) => !n);
|
||||
}, []);
|
||||
const onSettingsPressed = useCallback(() => {
|
||||
Navigation.push(componentId, { component: { name: 'Settings' } })
|
||||
Navigation.push(componentId, { component: { name: 'Settings' } });
|
||||
}, [componentId]);
|
||||
//#endregion
|
||||
|
||||
@@ -190,7 +167,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[isPressingButton, onFlipCameraPressed]
|
||||
[isPressingButton, onFlipCameraPressed],
|
||||
);
|
||||
//#endregion
|
||||
|
||||
@@ -199,12 +176,12 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
const loadDevices = async () => {
|
||||
try {
|
||||
const availableCameraDevices = await Camera.getAvailableCameraDevices();
|
||||
console.log(`Devices: ${availableCameraDevices.map((d) => d.name).join(", ")}`);
|
||||
console.log(`Devices: ${availableCameraDevices.map((d) => d.name).join(', ')}`);
|
||||
const sortedDevices = availableCameraDevices.sort(compareDevices);
|
||||
console.debug(`Devices (sorted): ${sortedDevices.map((d) => d.name).join(", ")}`);
|
||||
console.debug(`Devices (sorted): ${sortedDevices.map((d) => d.name).join(', ')}`);
|
||||
setDevices(sortedDevices);
|
||||
} catch (e) {
|
||||
console.error(`Failed to get available devices!`, e);
|
||||
console.error('Failed to get available devices!', e);
|
||||
}
|
||||
};
|
||||
loadDevices();
|
||||
@@ -216,9 +193,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
|
||||
useEffect(() => {
|
||||
// Run everytime the camera gets set to isActive = false. (reset zoom when tab switching)
|
||||
if (!isActive) {
|
||||
zoom.value = neutralZoomScaled;
|
||||
}
|
||||
if (!isActive) zoom.value = neutralZoomScaled;
|
||||
}, [neutralZoomScaled, isActive, zoom]);
|
||||
//#endregion
|
||||
|
||||
@@ -232,27 +207,19 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
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],
|
||||
[0, startZoom, 1],
|
||||
Extrapolate.CLAMP
|
||||
);
|
||||
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);
|
||||
},
|
||||
});
|
||||
//#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} @ ${fps}fps)`);
|
||||
console.log(
|
||||
`Re-rendering camera page with ${isActive ? 'active' : 'inactive'} camera. ` +
|
||||
`Device: "${device.name}" (${format.photoWidth}x${format.photoHeight} @ ${fps}fps)`,
|
||||
);
|
||||
} else {
|
||||
console.log(`re-rendering camera page without active camera`);
|
||||
console.log('re-rendering camera page without active camera');
|
||||
}
|
||||
|
||||
// TODO: Implement camera flipping (back <-> front) while recording and stich the videos together
|
||||
@@ -262,9 +229,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
{device != null && (
|
||||
<PinchGestureHandler onGestureEvent={onPinchGesture} enabled={isActive}>
|
||||
<Reanimated.View style={StyleSheet.absoluteFill}>
|
||||
<TapGestureHandler
|
||||
onHandlerStateChange={onDoubleTapGesture}
|
||||
numberOfTaps={2}>
|
||||
<TapGestureHandler onHandlerStateChange={onDoubleTapGesture} numberOfTaps={2}>
|
||||
<ReanimatedCamera
|
||||
ref={camera}
|
||||
style={StyleSheet.absoluteFill}
|
||||
@@ -272,9 +237,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
format={format}
|
||||
fps={fps}
|
||||
hdr={enableHdr}
|
||||
lowLightBoost={
|
||||
device.supportsLowLightBoost && enableNightMode
|
||||
}
|
||||
lowLightBoost={device.supportsLowLightBoost && enableNightMode}
|
||||
isActive={isActive}
|
||||
onInitialized={onInitialized}
|
||||
onError={onError}
|
||||
@@ -293,7 +256,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
camera={camera}
|
||||
onMediaCaptured={onMediaCaptured}
|
||||
cameraZoom={zoom}
|
||||
flash={supportsFlash ? flash : "off"}
|
||||
flash={supportsFlash ? flash : 'off'}
|
||||
enabled={isCameraInitialized && isActive}
|
||||
setIsPressingButton={setIsPressingButton}
|
||||
/>
|
||||
@@ -302,74 +265,45 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
|
||||
<View style={styles.rightButtonRow}>
|
||||
{supportsCameraFlipping && (
|
||||
<PressableOpacity
|
||||
style={styles.button}
|
||||
onPress={onFlipCameraPressed}
|
||||
disabledOpacity={0.4}>
|
||||
<IonIcon
|
||||
name="camera-reverse"
|
||||
color="white"
|
||||
size={24}
|
||||
/>
|
||||
<PressableOpacity style={styles.button} onPress={onFlipCameraPressed} disabledOpacity={0.4}>
|
||||
<IonIcon name="camera-reverse" color="white" size={24} />
|
||||
</PressableOpacity>
|
||||
)}
|
||||
{supportsFlash && (
|
||||
<PressableOpacity
|
||||
style={styles.button}
|
||||
onPress={onFlashPressed}
|
||||
disabledOpacity={0.4}>
|
||||
<IonIcon
|
||||
name={flash === "on" ? "flash" : "flash-off"}
|
||||
color="white"
|
||||
size={24}
|
||||
/>
|
||||
<PressableOpacity style={styles.button} onPress={onFlashPressed} disabledOpacity={0.4}>
|
||||
<IonIcon name={flash === 'on' ? 'flash' : 'flash-off'} color="white" size={24} />
|
||||
</PressableOpacity>
|
||||
)}
|
||||
{canToggleNightMode && (
|
||||
<PressableOpacity
|
||||
style={styles.button}
|
||||
onPress={onNightModePressed}
|
||||
disabledOpacity={0.4}>
|
||||
<IonIcon
|
||||
name={enableNightMode ? "moon" : "moon-outline"}
|
||||
color="white"
|
||||
size={24}
|
||||
/>
|
||||
<PressableOpacity style={styles.button} onPress={onNightModePressed} disabledOpacity={0.4}>
|
||||
<IonIcon name={enableNightMode ? 'moon' : 'moon-outline'} color="white" size={24} />
|
||||
</PressableOpacity>
|
||||
)}
|
||||
{supportsHdr && (
|
||||
<PressableOpacity style={styles.button} onPress={onHdrSwitchPressed}>
|
||||
<MaterialIcon
|
||||
name={enableHdr ? "hdr" : "hdr-off"}
|
||||
color="white"
|
||||
size={24}
|
||||
/>
|
||||
<MaterialIcon name={enableHdr ? 'hdr' : 'hdr-off'} color="white" size={24} />
|
||||
</PressableOpacity>
|
||||
)}
|
||||
<PressableOpacity style={styles.button} onPress={onSettingsPressed}>
|
||||
<IonIcon
|
||||
name="settings-outline"
|
||||
color="white"
|
||||
size={24}
|
||||
/>
|
||||
<IonIcon name="settings-outline" color="white" size={24} />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "black",
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
captureButton: {
|
||||
position: "absolute",
|
||||
alignSelf: "center",
|
||||
bottom: SAFE_AREA_PADDING.paddingBottom
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
bottom: SAFE_AREA_PADDING.paddingBottom,
|
||||
},
|
||||
openLocalGalleryButton: {
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
left: (SCREEN_WIDTH / 2 - CAPTURE_BUTTON_SIZE / 2) / 2,
|
||||
width: LOCAL_GALLERY_BUTTON_SIZE,
|
||||
height: LOCAL_GALLERY_BUTTON_SIZE,
|
||||
@@ -380,13 +314,13 @@ const styles = StyleSheet.create({
|
||||
width: BUTTON_SIZE,
|
||||
height: BUTTON_SIZE,
|
||||
borderRadius: BUTTON_SIZE / 2,
|
||||
backgroundColor: "rgba(140, 140, 140, 0.3)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: 'rgba(140, 140, 140, 0.3)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
rightButtonRow: {
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
right: CONTENT_SPACING,
|
||||
top: SAFE_AREA_PADDING.paddingTop
|
||||
top: SAFE_AREA_PADDING.paddingTop,
|
||||
},
|
||||
});
|
||||
|
@@ -1,18 +1,19 @@
|
||||
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;
|
||||
|
||||
const SAFE_BOTTOM = Platform.select({
|
||||
ios: StaticSafeAreaInsets.safeAreaInsetsBottom
|
||||
}) ?? 0
|
||||
const SAFE_BOTTOM =
|
||||
Platform.select({
|
||||
ios: StaticSafeAreaInsets.safeAreaInsetsBottom,
|
||||
}) ?? 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
|
||||
}
|
||||
paddingBottom: SAFE_BOTTOM + CONTENT_SPACING,
|
||||
};
|
||||
|
||||
// whether to use takeSnapshot() instead of takePhoto() on Android
|
||||
export const USE_SNAPSHOT_ON_ANDROID = false;
|
||||
@@ -35,8 +36,8 @@ export const MAX_ZOOM_FACTOR = 16;
|
||||
|
||||
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,
|
||||
android: Dimensions.get('screen').height - StaticSafeAreaInsets.safeAreaInsetsBottom,
|
||||
ios: Dimensions.get('window').height,
|
||||
}) as number;
|
||||
|
||||
// Capture Button
|
||||
|
@@ -1,14 +1,5 @@
|
||||
import {
|
||||
RESOLUTION_LIMIT,
|
||||
SCREEN_HEIGHT,
|
||||
SCREEN_WIDTH,
|
||||
USE_ULTRAWIDE_IF_AVAILABLE,
|
||||
} from "./Constants";
|
||||
import type {
|
||||
CameraDevice,
|
||||
CameraDeviceFormat,
|
||||
FrameRateRange,
|
||||
} from "react-native-vision-camera";
|
||||
import { RESOLUTION_LIMIT, SCREEN_HEIGHT, SCREEN_WIDTH, USE_ULTRAWIDE_IF_AVAILABLE } from './Constants';
|
||||
import type { CameraDevice, CameraDeviceFormat, FrameRateRange } from 'react-native-vision-camera';
|
||||
|
||||
/**
|
||||
* Compares two devices with the following criteria:
|
||||
@@ -23,14 +14,11 @@ import type {
|
||||
*
|
||||
* Note that this makes the `sort()` function descending, so the first element (`[0]`) is the "best" device.
|
||||
*/
|
||||
export const compareDevices = (
|
||||
left: CameraDevice,
|
||||
right: CameraDevice
|
||||
): -1 | 0 | 1 => {
|
||||
export const compareDevices = (left: CameraDevice, right: CameraDevice): -1 | 0 | 1 => {
|
||||
let leftPoints = 0;
|
||||
|
||||
const leftHasWideAngle = left.devices.includes("wide-angle-camera");
|
||||
const rightHasWideAngle = right.devices.includes("wide-angle-camera");
|
||||
const leftHasWideAngle = left.devices.includes('wide-angle-camera');
|
||||
const rightHasWideAngle = right.devices.includes('wide-angle-camera');
|
||||
if (leftHasWideAngle && !rightHasWideAngle) {
|
||||
// left does have a wide-angle-camera, but right doesn't.
|
||||
leftPoints += 5;
|
||||
@@ -40,12 +28,8 @@ export const compareDevices = (
|
||||
}
|
||||
|
||||
if (!USE_ULTRAWIDE_IF_AVAILABLE) {
|
||||
const leftHasUltraWideAngle = left.devices.includes(
|
||||
"ultra-wide-angle-camera"
|
||||
);
|
||||
const rightHasUltraWideAngle = right.devices.includes(
|
||||
"ultra-wide-angle-camera"
|
||||
);
|
||||
const leftHasUltraWideAngle = left.devices.includes('ultra-wide-angle-camera');
|
||||
const rightHasUltraWideAngle = right.devices.includes('ultra-wide-angle-camera');
|
||||
if (leftHasUltraWideAngle && !rightHasUltraWideAngle) {
|
||||
// left does have an ultra-wide-angle-camera, but right doesn't. Ultra-Wide cameras are bad because of their poor quality.
|
||||
leftPoints -= 5;
|
||||
@@ -76,7 +60,7 @@ const CAMERA_VIEW_SIZE: Size = {
|
||||
|
||||
const applyScaledMask = (
|
||||
clippedElementDimensions: Size, // 3024 x 4032 | 2160x3840
|
||||
maskDimensions: Size // 375 x 623
|
||||
maskDimensions: Size, // 375 x 623
|
||||
): Size => {
|
||||
const wScale = maskDimensions.width / clippedElementDimensions.width;
|
||||
const hScale = maskDimensions.height / clippedElementDimensions.height;
|
||||
@@ -111,10 +95,7 @@ const applyScaledMask = (
|
||||
*
|
||||
* Note that this makes the `sort()` function descending, so the first element (`[0]`) is the "best" format.
|
||||
*/
|
||||
export const compareFormats = (
|
||||
left: CameraDeviceFormat,
|
||||
right: CameraDeviceFormat
|
||||
): -1 | 0 | 1 => {
|
||||
export const compareFormats = (left: CameraDeviceFormat, right: CameraDeviceFormat): -1 | 0 | 1 => {
|
||||
// Point score of the left format. Higher is better.
|
||||
let leftPoints = 0;
|
||||
|
||||
@@ -122,8 +103,7 @@ export const compareFormats = (
|
||||
const rightPhotoPixels = right.photoHeight * right.photoWidth;
|
||||
if (leftPhotoPixels > rightPhotoPixels) {
|
||||
// left has greater photo dimensions
|
||||
const isLeftAbovePixelLimit =
|
||||
RESOLUTION_LIMIT != null && leftPhotoPixels > RESOLUTION_LIMIT;
|
||||
const isLeftAbovePixelLimit = RESOLUTION_LIMIT != null && leftPhotoPixels > RESOLUTION_LIMIT;
|
||||
if (isLeftAbovePixelLimit) {
|
||||
// left exceeds our pixel limit
|
||||
leftPoints -= 3;
|
||||
@@ -133,8 +113,7 @@ export const compareFormats = (
|
||||
}
|
||||
} else if (leftPhotoPixels < rightPhotoPixels) {
|
||||
// left has smaller photo dimensions
|
||||
const isRightAbovePixelLimit =
|
||||
RESOLUTION_LIMIT != null && rightPhotoPixels > RESOLUTION_LIMIT;
|
||||
const isRightAbovePixelLimit = RESOLUTION_LIMIT != null && rightPhotoPixels > RESOLUTION_LIMIT;
|
||||
if (isRightAbovePixelLimit) {
|
||||
// right exceeds our pixel limit
|
||||
leftPoints += 3;
|
||||
@@ -146,17 +125,14 @@ export const compareFormats = (
|
||||
|
||||
const leftCropped = applyScaledMask(
|
||||
{ width: left.photoHeight, height: left.photoWidth }, // cameras are horizontal, we rotate to portrait
|
||||
CAMERA_VIEW_SIZE
|
||||
CAMERA_VIEW_SIZE,
|
||||
);
|
||||
const rightCropped = applyScaledMask(
|
||||
{ width: right.photoHeight, height: right.photoWidth }, // cameras are horizontal, we rotate to portrait
|
||||
CAMERA_VIEW_SIZE
|
||||
CAMERA_VIEW_SIZE,
|
||||
);
|
||||
const leftOverflow =
|
||||
left.photoWidth * left.photoHeight - leftCropped.width * leftCropped.height;
|
||||
const rightOverflow =
|
||||
right.photoWidth * right.photoHeight -
|
||||
rightCropped.width * rightCropped.height;
|
||||
const leftOverflow = left.photoWidth * left.photoHeight - leftCropped.width * leftCropped.height;
|
||||
const rightOverflow = right.photoWidth * right.photoHeight - rightCropped.width * rightCropped.height;
|
||||
if (leftOverflow > rightOverflow) {
|
||||
// left has a higher overflow, aka more pixels that aren't on-screen and therefore wasted. Maybe left is 4:3 and right is 16:9
|
||||
leftPoints -= 4;
|
||||
@@ -165,12 +141,7 @@ export const compareFormats = (
|
||||
leftPoints += 4;
|
||||
}
|
||||
|
||||
if (
|
||||
left.videoHeight != null &&
|
||||
left.videoWidth != null &&
|
||||
right.videoHeight != null &&
|
||||
right.videoWidth != null
|
||||
) {
|
||||
if (left.videoHeight != null && left.videoWidth != null && right.videoHeight != null && right.videoWidth != null) {
|
||||
const leftVideoPixels = left.videoWidth * left.videoHeight ?? 0;
|
||||
const rightVideoPixels = right.videoWidth * right.videoHeight ?? 0;
|
||||
if (leftVideoPixels > rightVideoPixels) {
|
||||
@@ -182,12 +153,8 @@ export const compareFormats = (
|
||||
}
|
||||
}
|
||||
|
||||
const leftMaxFps = Math.max(
|
||||
...left.frameRateRanges.map((r) => r.maxFrameRate)
|
||||
);
|
||||
const rightMaxFps = Math.max(
|
||||
...right.frameRateRanges.map((r) => r.maxFrameRate)
|
||||
);
|
||||
const leftMaxFps = Math.max(...left.frameRateRanges.map((r) => r.maxFrameRate));
|
||||
const rightMaxFps = Math.max(...right.frameRateRanges.map((r) => r.maxFrameRate));
|
||||
if (leftMaxFps > rightMaxFps) {
|
||||
// left has more fps
|
||||
leftPoints += 2;
|
||||
@@ -220,42 +187,27 @@ export const compareFormats = (
|
||||
/**
|
||||
* Selects the smallest difference between a FrameRateRange's `maxFrameRate` and the given `fps`
|
||||
*/
|
||||
const smallestFpsDiff = (
|
||||
frameRateRanges: FrameRateRange[],
|
||||
fps: number
|
||||
): number => {
|
||||
const bestFrameRateRange = frameRateRanges.reduce<FrameRateRange | undefined>(
|
||||
(prev, curr) => {
|
||||
if (prev == null) return curr;
|
||||
const smallestFpsDiff = (frameRateRanges: FrameRateRange[], fps: number): number => {
|
||||
const bestFrameRateRange = frameRateRanges.reduce<FrameRateRange | undefined>((prev, curr) => {
|
||||
if (prev == null) return curr;
|
||||
|
||||
const prevDiff = Math.abs(prev.maxFrameRate - fps);
|
||||
const currDiff = Math.abs(curr.maxFrameRate - fps);
|
||||
if (prevDiff < currDiff) return prev;
|
||||
else return curr;
|
||||
},
|
||||
undefined
|
||||
);
|
||||
const prevDiff = Math.abs(prev.maxFrameRate - fps);
|
||||
const currDiff = Math.abs(curr.maxFrameRate - fps);
|
||||
if (prevDiff < currDiff) return prev;
|
||||
else return curr;
|
||||
}, undefined);
|
||||
const max = bestFrameRateRange?.maxFrameRate ?? 0;
|
||||
return Math.abs(max - fps);
|
||||
};
|
||||
|
||||
export const frameRateIncluded = (
|
||||
range: FrameRateRange,
|
||||
fps: number
|
||||
): boolean => fps >= range.minFrameRate && fps <= range.maxFrameRate;
|
||||
export const frameRateIncluded = (range: FrameRateRange, fps: number): boolean => fps >= range.minFrameRate && fps <= range.maxFrameRate;
|
||||
|
||||
const isFpsInFrameRateRange = (
|
||||
format: CameraDeviceFormat,
|
||||
fps: number
|
||||
): boolean => format.frameRateRanges.some((r) => frameRateIncluded(r, fps));
|
||||
const isFpsInFrameRateRange = (format: CameraDeviceFormat, fps: number): boolean => format.frameRateRanges.some((r) => frameRateIncluded(r, fps));
|
||||
|
||||
/**
|
||||
* Selects the format with the closest frame rate ranges to the FPS
|
||||
*/
|
||||
export const formatWithClosestMatchingFps = (
|
||||
formats: CameraDeviceFormat[],
|
||||
fps: number
|
||||
): CameraDeviceFormat | undefined =>
|
||||
export const formatWithClosestMatchingFps = (formats: CameraDeviceFormat[], fps: number): CameraDeviceFormat | undefined =>
|
||||
formats.reduce<CameraDeviceFormat | undefined>((prev, curr) => {
|
||||
if (prev == null) return curr;
|
||||
|
||||
|
@@ -12,42 +12,41 @@ import CameraRoll from '@react-native-community/cameraroll';
|
||||
import { StatusBarBlurBackground } from './views/StatusBarBlurBackground';
|
||||
|
||||
interface MediaProps {
|
||||
path: string,
|
||||
type: 'video' | 'photo'
|
||||
path: string;
|
||||
type: 'video' | 'photo';
|
||||
}
|
||||
|
||||
const requestSavePermission = async (): Promise<boolean> => {
|
||||
if (Platform.OS !== "android") {
|
||||
return true;
|
||||
}
|
||||
if (Platform.OS !== 'android') return true;
|
||||
|
||||
const permission = PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE;
|
||||
let hasPermission = await PermissionsAndroid.check(permission);
|
||||
if (!hasPermission) {
|
||||
const permissionRequestResult = await PermissionsAndroid.request(permission);
|
||||
hasPermission = permissionRequestResult === "granted";
|
||||
hasPermission = permissionRequestResult === 'granted';
|
||||
}
|
||||
return hasPermission;
|
||||
}
|
||||
};
|
||||
|
||||
export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, type, path }) => {
|
||||
const [hasMediaLoaded, setHasMediaLoaded] = useState(false);
|
||||
const isForeground = useIsForeground();
|
||||
const isScreenFocused = useIsScreenFocused(componentId);
|
||||
const isVideoPaused = !isForeground || !isScreenFocused;
|
||||
const [savingState, setSavingState] = useState<"none" | "saving" | "saved">("none");
|
||||
const [savingState, setSavingState] = useState<'none' | 'saving' | 'saved'>('none');
|
||||
|
||||
const onClosePressed = useCallback(() => {
|
||||
Navigation.dismissModal(componentId);
|
||||
}, [componentId]);
|
||||
|
||||
const onMediaLoadEnd = useCallback(() => {
|
||||
console.log(`media has loaded.`);
|
||||
console.log('media has loaded.');
|
||||
setHasMediaLoaded(true);
|
||||
}, []);
|
||||
|
||||
const onSavePressed = useCallback(async () => {
|
||||
try {
|
||||
setSavingState("saving");
|
||||
setSavingState('saving');
|
||||
|
||||
const hasPermission = await requestSavePermission();
|
||||
if (!hasPermission) {
|
||||
@@ -57,33 +56,23 @@ export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, ty
|
||||
await CameraRoll.save(`file://${path}`, {
|
||||
type: type,
|
||||
});
|
||||
setSavingState("saved");
|
||||
setSavingState('saved');
|
||||
} catch (e) {
|
||||
setSavingState("none");
|
||||
Alert.alert(
|
||||
"Failed to save!",
|
||||
`An unexpected error occured while trying to save your ${type}. ${e?.message ?? JSON.stringify(e)}`
|
||||
);
|
||||
setSavingState('none');
|
||||
Alert.alert('Failed to save!', `An unexpected error occured while trying to save your ${type}. ${e?.message ?? JSON.stringify(e)}`);
|
||||
}
|
||||
}, [path, type]);
|
||||
|
||||
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]}>
|
||||
{type === "photo" && (
|
||||
<Image
|
||||
{type === 'photo' && <Image source={source} style={StyleSheet.absoluteFill} resizeMode="cover" onLoadEnd={onMediaLoadEnd} />}
|
||||
{type === 'video' && (
|
||||
<Video
|
||||
source={source}
|
||||
style={StyleSheet.absoluteFill}
|
||||
resizeMode="cover"
|
||||
onLoadEnd={onMediaLoadEnd} />
|
||||
)}
|
||||
{type === "video" && (
|
||||
<Video source={source}
|
||||
style={StyleSheet.absoluteFill}
|
||||
paused={isVideoPaused}
|
||||
resizeMode="cover"
|
||||
@@ -96,48 +85,24 @@ export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, ty
|
||||
controls={false}
|
||||
playWhenInactive={true}
|
||||
ignoreSilentSwitch="ignore"
|
||||
onReadyForDisplay={onMediaLoadEnd} />
|
||||
onReadyForDisplay={onMediaLoadEnd}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PressableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={onClosePressed}>
|
||||
<IonIcon
|
||||
name="close"
|
||||
size={35}
|
||||
color="white"
|
||||
style={styles.icon} />
|
||||
<PressableOpacity style={styles.closeButton} onPress={onClosePressed}>
|
||||
<IonIcon name="close" size={35} color="white" style={styles.icon} />
|
||||
</PressableOpacity>
|
||||
|
||||
<PressableOpacity
|
||||
style={styles.saveButton}
|
||||
onPress={onSavePressed}
|
||||
disabled={savingState !== "none"}>
|
||||
{savingState === "none" && (
|
||||
<IonIcon
|
||||
name="download"
|
||||
size={35}
|
||||
color="white"
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
{savingState === "saved" && (
|
||||
<IonIcon
|
||||
name="checkmark"
|
||||
size={35}
|
||||
color="white"
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
{savingState === "saving" && (
|
||||
<ActivityIndicator color="white" />
|
||||
)}
|
||||
<PressableOpacity style={styles.saveButton} onPress={onSavePressed} disabled={savingState !== 'none'}>
|
||||
{savingState === 'none' && <IonIcon name="download" size={35} color="white" style={styles.icon} />}
|
||||
{savingState === 'saved' && <IonIcon name="checkmark" size={35} color="white" style={styles.icon} />}
|
||||
{savingState === 'saving' && <ActivityIndicator color="white" />}
|
||||
</PressableOpacity>
|
||||
|
||||
<StatusBarBlurBackground />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Media.options = {
|
||||
modal: {
|
||||
@@ -154,8 +119,8 @@ Media.options = {
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
backgroundColor: "transparent",
|
||||
componentBackgroundColor: "transparent",
|
||||
backgroundColor: 'transparent',
|
||||
componentBackgroundColor: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -181,7 +146,7 @@ const styles = StyleSheet.create({
|
||||
height: 40,
|
||||
},
|
||||
icon: {
|
||||
textShadowColor: "black",
|
||||
textShadowColor: 'black',
|
||||
textShadowOffset: {
|
||||
height: 0,
|
||||
width: 0,
|
||||
|
@@ -3,33 +3,36 @@ import React, { useCallback } from 'react';
|
||||
import { StyleSheet, View, Text, Linking } from 'react-native';
|
||||
import type { NavigationFunctionComponent } from 'react-native-navigation';
|
||||
|
||||
|
||||
export const Settings: NavigationFunctionComponent = () => {
|
||||
const onCuventPressed = useCallback(() => {
|
||||
Linking.openURL('https://cuvent.com')
|
||||
Linking.openURL('https://cuvent.com');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.aboutText}>Vision Camera is powered by{" "}
|
||||
<Text style={styles.hyperlink} onPress={onCuventPressed}>Cuvent</Text>.
|
||||
<Text style={styles.aboutText}>
|
||||
Vision Camera is powered by{' '}
|
||||
<Text style={styles.hyperlink} onPress={onCuventPressed}>
|
||||
Cuvent
|
||||
</Text>
|
||||
.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Settings.options = {
|
||||
topBar: {
|
||||
visible: true,
|
||||
title: {
|
||||
text: 'Settings'
|
||||
text: 'Settings',
|
||||
},
|
||||
backButton: {
|
||||
id: 'back',
|
||||
showTitle: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@@ -1,19 +1,13 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { StyleSheet, View, Text, Image } from 'react-native';
|
||||
import { StyleSheet, View, Text, Image } from 'react-native';
|
||||
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
||||
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera';
|
||||
import { CONTENT_SPACING, SAFE_AREA_PADDING } from './Constants';
|
||||
|
||||
export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
||||
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...');
|
||||
@@ -31,14 +25,12 @@ export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const checkPermissions = async () => {
|
||||
console.log(`Checking Permission status...`);
|
||||
let [cameraPermission, microphonePermission] = await Promise.all([
|
||||
console.log('Checking Permission status...');
|
||||
const [cameraPermission, microphonePermission] = await Promise.all([
|
||||
Camera.getCameraPermissionStatus(),
|
||||
Camera.getMicrophonePermissionStatus(),
|
||||
]);
|
||||
console.log(
|
||||
`Check: CameraPermission: ${cameraPermission} | MicrophonePermission: ${microphonePermission}`
|
||||
);
|
||||
console.log(`Check: CameraPermission: ${cameraPermission} | MicrophonePermission: ${microphonePermission}`);
|
||||
setCameraPermissionStatus(cameraPermission);
|
||||
setMicrophonePermissionStatus(microphonePermission);
|
||||
};
|
||||
@@ -54,13 +46,13 @@ export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
||||
children: [
|
||||
{
|
||||
component: {
|
||||
name: 'Home'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
name: 'Home',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [cameraPermissionStatus, microphonePermissionStatus, componentId]);
|
||||
|
||||
@@ -71,17 +63,25 @@ export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
||||
|
||||
<View style={styles.permissionsContainer}>
|
||||
{cameraPermissionStatus !== 'authorized' && (
|
||||
<Text style={styles.permissionText}>Vision Camera needs <Text style={styles.bold}>Camera permission</Text>.
|
||||
<Text style={styles.hyperlink} onPress={requestCameraPermission}>Grant</Text></Text>
|
||||
<Text style={styles.permissionText}>
|
||||
Vision Camera needs <Text style={styles.bold}>Camera permission</Text>.
|
||||
<Text style={styles.hyperlink} onPress={requestCameraPermission}>
|
||||
Grant
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
{microphonePermissionStatus !== 'authorized' && (
|
||||
<Text style={styles.permissionText}>Vision Camera needs <Text style={styles.bold}>Microphone permission</Text>.
|
||||
<Text style={styles.hyperlink} onPress={requestMicrophonePermission}>Grant</Text></Text>
|
||||
<Text style={styles.permissionText}>
|
||||
Vision Camera needs <Text style={styles.bold}>Microphone permission</Text>.
|
||||
<Text style={styles.hyperlink} onPress={requestMicrophonePermission}>
|
||||
Grant
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
welcome: {
|
||||
@@ -98,10 +98,10 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: 'white',
|
||||
...SAFE_AREA_PADDING
|
||||
...SAFE_AREA_PADDING,
|
||||
},
|
||||
permissionsContainer: {
|
||||
marginTop: CONTENT_SPACING *2
|
||||
marginTop: CONTENT_SPACING * 2,
|
||||
},
|
||||
permissionText: {
|
||||
fontSize: 17,
|
||||
|
@@ -1,23 +1,17 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Same as `useState`, but swallow all calls to `setState` if the value didn't change (uses `===` comparator per default)
|
||||
* @param initialValue The initial state
|
||||
* @param comparator A custom comparator, useful if you want to only round numbers or use string locale for comparison. Make sure this function is memoized!
|
||||
*/
|
||||
export const useCachedState = <T>(
|
||||
initialValue: T,
|
||||
comparator?: (oldState: T, newState: T) => boolean
|
||||
): [T, (newState: T) => void] => {
|
||||
export const useCachedState = <T>(initialValue: T, comparator?: (oldState: T, newState: T) => boolean): [T, (newState: T) => void] => {
|
||||
const [state, setState] = useState(initialValue);
|
||||
const cachedState = useRef(initialValue);
|
||||
|
||||
const dispatchState = useCallback(
|
||||
(newState: T) => {
|
||||
const areEqual =
|
||||
comparator == null
|
||||
? cachedState.current === newState
|
||||
: comparator(cachedState.current, newState);
|
||||
const areEqual = comparator == null ? cachedState.current === newState : comparator(cachedState.current, newState);
|
||||
if (areEqual) {
|
||||
return;
|
||||
} else {
|
||||
@@ -25,7 +19,7 @@ export const useCachedState = <T>(
|
||||
setState(newState);
|
||||
}
|
||||
},
|
||||
[comparator]
|
||||
[comparator],
|
||||
);
|
||||
|
||||
return [state, dispatchState];
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { useEffect } from "react"
|
||||
import { AppState, AppStateStatus } from "react-native";
|
||||
import { useCachedState } from "./useCachedState";
|
||||
import { useEffect } from 'react';
|
||||
import { AppState, AppStateStatus } from 'react-native';
|
||||
import { useCachedState } from './useCachedState';
|
||||
|
||||
export const useIsForeground = (): boolean => {
|
||||
const [isForeground, setIsForeground] = useCachedState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (state: AppStateStatus) => {
|
||||
setIsForeground(state === "active");
|
||||
setIsForeground(state === 'active');
|
||||
};
|
||||
AppState.addEventListener("change", onChange);
|
||||
return () => AppState.removeEventListener("change", onChange);
|
||||
AppState.addEventListener('change', onChange);
|
||||
return () => AppState.removeEventListener('change', onChange);
|
||||
}, [setIsForeground]);
|
||||
|
||||
return isForeground;
|
||||
}
|
||||
};
|
||||
|
@@ -1,23 +1,23 @@
|
||||
import { useEffect, useMemo, useReducer } from "react";
|
||||
import { Navigation } from "react-native-navigation";
|
||||
import { useEffect, useMemo, useReducer } from 'react';
|
||||
import { Navigation } from 'react-native-navigation';
|
||||
|
||||
type Action =
|
||||
| {
|
||||
action: "push";
|
||||
action: 'push';
|
||||
componentId: string;
|
||||
}
|
||||
| {
|
||||
action: "pop";
|
||||
action: 'pop';
|
||||
componentId: string;
|
||||
};
|
||||
|
||||
const reducer = (stack: string[], action: Action): string[] => {
|
||||
switch (action.action) {
|
||||
case "push": {
|
||||
case 'push': {
|
||||
stack.push(action.componentId);
|
||||
break;
|
||||
}
|
||||
case "pop": {
|
||||
case 'pop': {
|
||||
const index = stack.indexOf(action.componentId);
|
||||
if (index > -1) stack.splice(index, 1);
|
||||
break;
|
||||
@@ -28,34 +28,27 @@ const reducer = (stack: string[], action: Action): string[] => {
|
||||
|
||||
export const useIsScreenFocused = (componentId: string): boolean => {
|
||||
const [componentStack, dispatch] = useReducer(reducer, [componentId]);
|
||||
const isFocussed = useMemo(
|
||||
() => componentStack[componentStack.length - 1] === componentId,
|
||||
[componentStack, componentId]
|
||||
);
|
||||
const isFocussed = useMemo(() => componentStack[componentStack.length - 1] === componentId, [componentStack, componentId]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = Navigation.events().registerComponentDidAppearListener(
|
||||
(event) => {
|
||||
if (event.componentType !== "Component") return;
|
||||
dispatch({
|
||||
action: "push",
|
||||
componentId: event.componentId,
|
||||
});
|
||||
}
|
||||
);
|
||||
const listener = Navigation.events().registerComponentDidAppearListener((event) => {
|
||||
if (event.componentType !== 'Component') return;
|
||||
dispatch({
|
||||
action: 'push',
|
||||
componentId: event.componentId,
|
||||
});
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const listener = Navigation.events().registerComponentDidDisappearListener(
|
||||
(event) => {
|
||||
if (event.componentType !== "Component") return;
|
||||
dispatch({
|
||||
action: "pop",
|
||||
componentId: event.componentId,
|
||||
});
|
||||
}
|
||||
);
|
||||
const listener = Navigation.events().registerComponentDidDisappearListener((event) => {
|
||||
if (event.componentType !== 'Component') return;
|
||||
dispatch({
|
||||
action: 'pop',
|
||||
componentId: event.componentId,
|
||||
});
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import React, { useCallback, useMemo, useRef } from "react";
|
||||
import { Platform, StyleSheet, View, ViewProps } from "react-native";
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { Platform, 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,28 +18,25 @@ import Reanimated, {
|
||||
useAnimatedGestureHandler,
|
||||
useSharedValue,
|
||||
withRepeat,
|
||||
} from "react-native-reanimated";
|
||||
import type { Camera, PhotoFile, TakePhotoOptions, TakeSnapshotOptions, VideoFile } from "react-native-vision-camera";
|
||||
import { CAPTURE_BUTTON_SIZE, SCREEN_HEIGHT, SCREEN_WIDTH, USE_SNAPSHOT_ON_ANDROID } from "./../Constants";
|
||||
} from 'react-native-reanimated';
|
||||
import type { Camera, PhotoFile, TakePhotoOptions, TakeSnapshotOptions, VideoFile } from 'react-native-vision-camera';
|
||||
import { CAPTURE_BUTTON_SIZE, SCREEN_HEIGHT, SCREEN_WIDTH, USE_SNAPSHOT_ON_ANDROID } from './../Constants';
|
||||
|
||||
const PAN_GESTURE_HANDLER_FAIL_X = [-SCREEN_WIDTH, SCREEN_WIDTH];
|
||||
const PAN_GESTURE_HANDLER_ACTIVE_Y = [-2, 2];
|
||||
|
||||
const IS_ANDROID = Platform.OS === "android";
|
||||
const IS_ANDROID = Platform.OS === 'android';
|
||||
|
||||
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;
|
||||
onMediaCaptured: (media: PhotoFile | VideoFile, type: 'photo' | 'video') => void;
|
||||
|
||||
cameraZoom: Reanimated.SharedValue<number>;
|
||||
|
||||
flash: "off" | "on" | "auto";
|
||||
flash: 'off' | 'on' | 'auto';
|
||||
|
||||
enabled: boolean;
|
||||
|
||||
@@ -61,36 +58,29 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
const recordingProgress = useSharedValue(0);
|
||||
const takePhotoOptions = useMemo<TakePhotoOptions & TakeSnapshotOptions>(
|
||||
() => ({
|
||||
photoCodec: "jpeg",
|
||||
qualityPrioritization: "speed",
|
||||
photoCodec: 'jpeg',
|
||||
qualityPrioritization: 'speed',
|
||||
flash: flash,
|
||||
quality: 90,
|
||||
skipMetadata: true,
|
||||
}),
|
||||
[flash]
|
||||
[flash],
|
||||
);
|
||||
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!');
|
||||
|
||||
// If we're on Android and flash is disabled, we can use the "snapshot" method.
|
||||
// this will take a snapshot of the current SurfaceView, which results in faster image
|
||||
// capture rate at the cost of greatly reduced quality.
|
||||
const photoMethod =
|
||||
USE_SNAPSHOT_ON_ANDROID &&
|
||||
IS_ANDROID &&
|
||||
takePhotoOptions.flash === "off"
|
||||
? "snapshot"
|
||||
: "photo";
|
||||
const photoMethod = USE_SNAPSHOT_ON_ANDROID && IS_ANDROID && takePhotoOptions.flash === 'off' ? 'snapshot' : 'photo';
|
||||
console.log(`Taking ${photoMethod}...`);
|
||||
const photo =
|
||||
photoMethod === "snapshot"
|
||||
? await camera.current.takeSnapshot(takePhotoOptions)
|
||||
: await camera.current.takePhoto(takePhotoOptions);
|
||||
onMediaCaptured(photo, "photo");
|
||||
photoMethod === 'snapshot' ? await camera.current.takeSnapshot(takePhotoOptions) : await camera.current.takePhoto(takePhotoOptions);
|
||||
onMediaCaptured(photo, 'photo');
|
||||
} catch (e) {
|
||||
console.error('Failed to take photo!', e);
|
||||
}
|
||||
@@ -99,24 +89,24 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
const onStoppedRecording = useCallback(() => {
|
||||
isRecording.current = false;
|
||||
cancelAnimation(recordingProgress);
|
||||
console.log(`stopped recording video!`);
|
||||
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()...");
|
||||
console.log('calling stopRecording()...');
|
||||
await camera.current.stopRecording();
|
||||
console.log("called stopRecording()!");
|
||||
console.log('called stopRecording()!');
|
||||
} catch (e) {
|
||||
console.error(`failed to stop recording!`, e);
|
||||
console.error('failed to stop recording!', e);
|
||||
}
|
||||
}, [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) => {
|
||||
@@ -125,24 +115,17 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
},
|
||||
onRecordingFinished: (video) => {
|
||||
console.log(`Recording successfully finished! ${video.path}`);
|
||||
onMediaCaptured(video, "video");
|
||||
onMediaCaptured(video, 'video');
|
||||
onStoppedRecording();
|
||||
},
|
||||
});
|
||||
// TODO: wait until startRecording returns to actually find out if the recording has successfully started
|
||||
console.log(`called startRecording()!`);
|
||||
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,
|
||||
recordingProgress,
|
||||
stopRecording,
|
||||
]);
|
||||
}, [camera, flash, onMediaCaptured, onStoppedRecording, recordingProgress, stopRecording]);
|
||||
//#endregion
|
||||
|
||||
//#region Tap handler
|
||||
@@ -180,8 +163,7 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
case State.CANCELLED: {
|
||||
// exit "recording mode"
|
||||
try {
|
||||
if (pressDownDate.current == null)
|
||||
throw new Error("PressDownDate ref .current was null!");
|
||||
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;
|
||||
@@ -204,46 +186,26 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
break;
|
||||
}
|
||||
},
|
||||
[
|
||||
isPressingButton,
|
||||
recordingProgress,
|
||||
setIsPressingButton,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
takePhoto,
|
||||
]
|
||||
[isPressingButton, recordingProgress, setIsPressingButton, startRecording, stopRecording, takePhoto],
|
||||
);
|
||||
//#endregion
|
||||
//#region Pan handler
|
||||
const panHandler = useRef<PanGestureHandler>();
|
||||
const onPanGestureEvent = useAnimatedGestureHandler<
|
||||
PanGestureHandlerGestureEvent,
|
||||
{ offsetY?: number; startY?: number }
|
||||
>({
|
||||
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;
|
||||
|
||||
// extrapolate [0 ... 1] zoom -> [0 ... Y_FOR_FULL_ZOOM] finger position
|
||||
context.offsetY = interpolate(
|
||||
Math.sqrt(cameraZoom.value),
|
||||
[0, 1],
|
||||
[0, offsetYForFullZoom],
|
||||
Extrapolate.CLAMP
|
||||
);
|
||||
context.offsetY = interpolate(Math.sqrt(cameraZoom.value), [0, 1], [0, offsetYForFullZoom], Extrapolate.CLAMP);
|
||||
},
|
||||
onActive: (event, context) => {
|
||||
const offset = context.offsetY ?? 0;
|
||||
const startY = context.startY ?? SCREEN_HEIGHT;
|
||||
const yForFullZoom = startY * 0.7;
|
||||
|
||||
const zoom = interpolate(
|
||||
event.absoluteY - offset,
|
||||
[yForFullZoom, startY],
|
||||
[1, 0],
|
||||
Extrapolate.CLAMP
|
||||
);
|
||||
const zoom = interpolate(event.absoluteY - offset, [yForFullZoom, startY], [1, 0], Extrapolate.CLAMP);
|
||||
cameraZoom.value = zoom ** 2;
|
||||
},
|
||||
});
|
||||
@@ -261,44 +223,45 @@ const _CaptureButton: React.FC<Props> = ({
|
||||
},
|
||||
],
|
||||
}),
|
||||
[isPressingButton]
|
||||
[isPressingButton],
|
||||
);
|
||||
const buttonStyle = useAnimatedStyle(
|
||||
() => {
|
||||
let scale: number;
|
||||
if (enabled) {
|
||||
if (isPressingButton.value) {
|
||||
scale = withRepeat(withSpring(1, {
|
||||
const buttonStyle = useAnimatedStyle(() => {
|
||||
let scale: number;
|
||||
if (enabled) {
|
||||
if (isPressingButton.value) {
|
||||
scale = withRepeat(
|
||||
withSpring(1, {
|
||||
stiffness: 100,
|
||||
damping: 1000,
|
||||
}), -1, true);
|
||||
} else {
|
||||
scale = withSpring(0.9, {
|
||||
stiffness: 500,
|
||||
damping: 300,
|
||||
});
|
||||
}
|
||||
}),
|
||||
-1,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
scale = withSpring(0.6, {
|
||||
scale = withSpring(0.9, {
|
||||
stiffness: 500,
|
||||
damping: 300,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
scale = withSpring(0.6, {
|
||||
stiffness: 500,
|
||||
damping: 300,
|
||||
});
|
||||
}
|
||||
|
||||
return ({
|
||||
opacity: withTiming(enabled ? 1 : 0.3, {
|
||||
duration: 100,
|
||||
easing: Easing.linear,
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
scale: scale
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
[enabled, isPressingButton]
|
||||
);
|
||||
return {
|
||||
opacity: withTiming(enabled ? 1 : 0.3, {
|
||||
duration: 100,
|
||||
easing: Easing.linear,
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
scale: scale,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [enabled, isPressingButton]);
|
||||
|
||||
return (
|
||||
<TapGestureHandler
|
||||
@@ -333,17 +296,17 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
},
|
||||
shadow: {
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
width: CAPTURE_BUTTON_SIZE,
|
||||
height: CAPTURE_BUTTON_SIZE,
|
||||
borderRadius: CAPTURE_BUTTON_SIZE / 2,
|
||||
backgroundColor: "#e34077",
|
||||
backgroundColor: '#e34077',
|
||||
},
|
||||
button: {
|
||||
width: CAPTURE_BUTTON_SIZE,
|
||||
height: CAPTURE_BUTTON_SIZE,
|
||||
borderRadius: CAPTURE_BUTTON_SIZE / 2,
|
||||
borderWidth: BORDER_WIDTH,
|
||||
borderColor: "white",
|
||||
borderColor: 'white',
|
||||
},
|
||||
});
|
||||
|
@@ -1,11 +1,5 @@
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
PressableProps,
|
||||
Pressable,
|
||||
PressableStateCallbackType,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
} from "react-native";
|
||||
import React, { useCallback } from 'react';
|
||||
import { PressableProps, Pressable, PressableStateCallbackType, StyleProp, ViewStyle } from 'react-native';
|
||||
|
||||
export interface PressableOpacityProps extends PressableProps {
|
||||
/**
|
||||
@@ -22,9 +16,7 @@ export interface PressableOpacityProps extends PressableProps {
|
||||
activeOpacity?: number;
|
||||
}
|
||||
|
||||
export type StyleType = (
|
||||
state: PressableStateCallbackType
|
||||
) => StyleProp<ViewStyle>;
|
||||
export type StyleType = (state: PressableStateCallbackType) => StyleProp<ViewStyle>;
|
||||
|
||||
/**
|
||||
* A Pressable component that lowers opacity when in pressed state. Uses the JS Pressability API.
|
||||
@@ -45,12 +37,9 @@ export const PressableOpacity = ({
|
||||
else return 1;
|
||||
}
|
||||
},
|
||||
[activeOpacity, disabled, disabledOpacity]
|
||||
);
|
||||
const _style = useCallback<StyleType>(
|
||||
({ pressed }) => [style as ViewStyle, { opacity: getOpacity(pressed) }],
|
||||
[getOpacity, style]
|
||||
[activeOpacity, disabled, disabledOpacity],
|
||||
);
|
||||
const _style = useCallback<StyleType>(({ pressed }) => [style as ViewStyle, { opacity: getOpacity(pressed) }], [getOpacity, style]);
|
||||
|
||||
return <Pressable style={_style} disabled={disabled} {...passThroughProps} />;
|
||||
};
|
||||
|
@@ -1,14 +1,11 @@
|
||||
import { BlurView, BlurViewProperties } 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, BlurViewProperties } 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
|
||||
}: BlurViewProperties) => {
|
||||
const StatusBarBlurBackgroundImpl = ({ style, ...props }: BlurViewProperties) => {
|
||||
if (Platform.OS !== 'ios') return null;
|
||||
|
||||
return (
|
||||
@@ -26,7 +23,7 @@ export const StatusBarBlurBackground = React.memo(StatusBarBlurBackgroundImpl);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
statusBarBackground: {
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
|
Reference in New Issue
Block a user