ESLint autofix

This commit is contained in:
Marc Rousavy 2021-02-20 17:07:10 +01:00
parent 50509200aa
commit dc2be934f6
22 changed files with 439 additions and 690 deletions

View File

@ -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,
},
},
],
},
},
});
});

View File

@ -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);

View File

@ -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,18 +114,17 @@ 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") => {
const onMediaCaptured = useCallback(async (media: PhotoFile | VideoFile, type: 'photo' | 'video') => {
console.log(`Media captured! ${JSON.stringify(media)}`);
await Navigation.showModal({
component: {
@ -153,26 +132,24 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
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,
},
});

View File

@ -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

View File

@ -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) => {
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
);
}, 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;

View File

@ -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,

View File

@ -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: {

View File

@ -6,14 +6,8 @@ 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,

View File

@ -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];

View File

@ -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;
}
};

View File

@ -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;
const listener = Navigation.events().registerComponentDidAppearListener((event) => {
if (event.componentType !== 'Component') return;
dispatch({
action: "push",
action: 'push',
componentId: event.componentId,
});
}
);
});
return () => listener.remove();
}, []);
useEffect(() => {
const listener = Navigation.events().registerComponentDidDisappearListener(
(event) => {
if (event.componentType !== "Component") return;
const listener = Navigation.events().registerComponentDidDisappearListener((event) => {
if (event.componentType !== 'Component') return;
dispatch({
action: "pop",
action: 'pop',
componentId: event.componentId,
});
}
);
});
return () => listener.remove();
}, []);

View File

@ -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,17 +223,20 @@ const _CaptureButton: React.FC<Props> = ({
},
],
}),
[isPressingButton]
[isPressingButton],
);
const buttonStyle = useAnimatedStyle(
() => {
const buttonStyle = useAnimatedStyle(() => {
let scale: number;
if (enabled) {
if (isPressingButton.value) {
scale = withRepeat(withSpring(1, {
scale = withRepeat(
withSpring(1, {
stiffness: 100,
damping: 1000,
}), -1, true);
}),
-1,
true,
);
} else {
scale = withSpring(0.9, {
stiffness: 500,
@ -285,20 +250,18 @@ const _CaptureButton: React.FC<Props> = ({
});
}
return ({
return {
opacity: withTiming(enabled ? 1 : 0.3, {
duration: 100,
easing: Easing.linear,
}),
transform: [
{
scale: scale
scale: scale,
},
],
})
},
[enabled, isPressingButton]
);
};
}, [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',
},
});

View File

@ -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} />;
};

View File

@ -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,

View File

@ -12,15 +12,15 @@
* * `"hevc-alpha"`: The HEVC (`muxa`) video codec that supports an alpha channel. This constant is used to select the appropriate encoder, but is NOT used on the encoded content, which is backwards compatible and hence uses `"hvc1"` as its codec type. _(iOS 13.0+)_
*/
export type CameraVideoCodec =
| "h264"
| "hevc"
| "hevc-alpha"
| "jpeg"
| "pro-res-4444"
| "pro-res-422"
| "pro-res-422-hq"
| "pro-res-422-lt"
| "pro-res-422-proxy";
| 'h264'
| 'hevc'
| 'hevc-alpha'
| 'jpeg'
| 'pro-res-4444'
| 'pro-res-422'
| 'pro-res-422-hq'
| 'pro-res-422-lt'
| 'pro-res-422-proxy';
// TODO: Support RAW photo codec
/**
@ -30,4 +30,4 @@ export type CameraVideoCodec =
* * `"jpeg"`: The JPEG (`jpeg`) video codec. _(iOS 11.0+)_
* * `"hevc-alpha"`: The HEVC (`muxa`) video codec that supports an alpha channel. This constant is used to select the appropriate encoder, but is NOT used on the encoded content, which is backwards compatible and hence uses `"hvc1"` as its codec type. _(iOS 13.0+)_
*/
export type CameraPhotoCodec = "hevc" | "jpeg" | "hevc-alpha";
export type CameraPhotoCodec = 'hevc' | 'jpeg' | 'hevc-alpha';

View File

@ -1,4 +1,4 @@
import type { CameraPosition } from "./CameraPosition";
import type { CameraPosition } from './CameraPosition';
/**
* Indentifiers for a physical camera (one that actually exists on the back/front of the device)
@ -7,10 +7,7 @@ import type { CameraPosition } from "./CameraPosition";
* * `"wide-angle-camera"`: A built-in wide-angle camera. (focal length between 24mm and 35mm)
* * `"telephoto-camera"`: A built-in camera device with a longer focal length than a wide-angle camera. (focal length between above 85mm)
*/
export type PhysicalCameraDeviceType =
| "ultra-wide-angle-camera"
| "wide-angle-camera"
| "telephoto-camera";
export type PhysicalCameraDeviceType = 'ultra-wide-angle-camera' | 'wide-angle-camera' | 'telephoto-camera';
/**
* Indentifiers for a logical camera (Combinations of multiple physical cameras to create a single logical camera).
@ -20,38 +17,24 @@ export type PhysicalCameraDeviceType =
* * `"triple-camera"`: A device that consists of three cameras of fixed focal length, one ultrawide angle, one wide angle, and one telephoto.
* * `"true-depth-camera"`: A combination of cameras and other sensors that creates a capture device capable of photo, video, and depth capture.
*/
export type LogicalCameraDeviceType =
| "dual-camera"
| "dual-wide-camera"
| "triple-camera"
| "true-depth-camera";
export type LogicalCameraDeviceType = 'dual-camera' | 'dual-wide-camera' | 'triple-camera' | 'true-depth-camera';
/**
* Parses an array of physical device types into a single `PhysicalCameraDeviceType` or `LogicalCameraDeviceType`, depending what matches.
*/
export const parsePhysicalDeviceTypes = (
physicalDeviceTypes: PhysicalCameraDeviceType[]
): PhysicalCameraDeviceType | LogicalCameraDeviceType => {
if (physicalDeviceTypes.length === 1) {
return physicalDeviceTypes[0];
}
const hasWide = physicalDeviceTypes.includes("wide-angle-camera");
const hasUltra = physicalDeviceTypes.includes("ultra-wide-angle-camera");
const hasTele = physicalDeviceTypes.includes("telephoto-camera");
if (hasTele && hasWide && hasUltra) {
return "triple-camera";
}
if (hasWide && hasUltra) {
return "dual-wide-camera";
}
if (hasWide && hasTele) {
return "dual-camera";
}
throw new Error(
`Invalid physical device type combination! ${physicalDeviceTypes.join(
" + "
)}`
);
export const parsePhysicalDeviceTypes = (physicalDeviceTypes: PhysicalCameraDeviceType[]): PhysicalCameraDeviceType | LogicalCameraDeviceType => {
if (physicalDeviceTypes.length === 1) return physicalDeviceTypes[0];
const hasWide = physicalDeviceTypes.includes('wide-angle-camera');
const hasUltra = physicalDeviceTypes.includes('ultra-wide-angle-camera');
const hasTele = physicalDeviceTypes.includes('telephoto-camera');
if (hasTele && hasWide && hasUltra) return 'triple-camera';
if (hasWide && hasUltra) return 'dual-wide-camera';
if (hasWide && hasTele) return 'dual-camera';
throw new Error(`Invalid physical device type combination! ${physicalDeviceTypes.join(' + ')}`);
};
/**
@ -65,7 +48,7 @@ export const parsePhysicalDeviceTypes = (
* #### The following colorspaces are available on Android:
* * `"yuv"`: The YCbCr color space.
*/
export type ColorSpace = "hlg-bt2020" | "p3-d65" | "srgb" | "yuv";
export type ColorSpace = 'hlg-bt2020' | 'p3-d65' | 'srgb' | 'yuv';
/**
* Indicates a format's autofocus system.
@ -74,7 +57,7 @@ export type ColorSpace = "hlg-bt2020" | "p3-d65" | "srgb" | "yuv";
* * `"contrast-detection"`: Indicates that autofocus is achieved by contrast detection. Contrast detection performs a focus scan to find the optimal position
* * `"phase-detection"`: Indicates that autofocus is achieved by phase detection. Phase detection has the ability to achieve focus in many cases without a focus scan. Phase detection autofocus is typically less visually intrusive than contrast detection autofocus
*/
export type AutoFocusSystem = "contrast-detection" | "phase-detection" | "none";
export type AutoFocusSystem = 'contrast-detection' | 'phase-detection' | 'none';
/**
* Indicates a format's supported video stabilization mode
@ -85,12 +68,7 @@ export type AutoFocusSystem = "contrast-detection" | "phase-detection" | "none";
* * `"cinematic-extended"`: Indicates that the video should be stabilized using the extended cinematic stabilization algorithm. Enabling extended cinematic stabilization introduces longer latency into the video capture pipeline compared to the AVCaptureVideoStabilizationModeCinematic and consumes more memory, but yields improved stability. It is recommended to use identical or similar min and max frame durations in conjunction with this mode (iOS 13.0+)
* * `"auto"`: Indicates that the most appropriate video stabilization mode for the device and format should be chosen automatically
*/
export type VideoStabilizationMode =
| "off"
| "standard"
| "cinematic"
| "cinematic-extended"
| "auto";
export type VideoStabilizationMode = 'off' | 'standard' | 'cinematic' | 'cinematic-extended' | 'auto';
export type FrameRateRange = Readonly<{
minFrameRate: number;

View File

@ -1,42 +1,40 @@
export type PermissionError =
| "permission/microphone-permission-denied"
| "permission/camera-permission-denied";
export type PermissionError = 'permission/microphone-permission-denied' | 'permission/camera-permission-denied';
export type ParameterError =
| "parameter/invalid-parameter"
| "parameter/unsupported-os"
| "parameter/unsupported-output"
| "parameter/unsupported-input"
| "parameter/invalid-combination";
| 'parameter/invalid-parameter'
| 'parameter/unsupported-os'
| 'parameter/unsupported-output'
| 'parameter/unsupported-input'
| 'parameter/invalid-combination';
export type DeviceError =
| "device/configuration-error"
| "device/no-device"
| "device/invalid-device"
| "device/torch-unavailable"
| "device/microphone-unavailable"
| "device/low-light-boost-not-supported"
| "device/focus-not-supported"
| "device/camera-not-available-on-simulator";
| 'device/configuration-error'
| 'device/no-device'
| 'device/invalid-device'
| 'device/torch-unavailable'
| 'device/microphone-unavailable'
| 'device/low-light-boost-not-supported'
| 'device/focus-not-supported'
| 'device/camera-not-available-on-simulator';
export type FormatError =
| "format/invalid-fps"
| "format/invalid-hdr"
| "format/invalid-low-light-boost"
| "format/invalid-format"
| "format/invalid-preset";
export type SessionError = "session/camera-not-ready";
| 'format/invalid-fps'
| 'format/invalid-hdr'
| 'format/invalid-low-light-boost'
| 'format/invalid-format'
| 'format/invalid-preset';
export type SessionError = 'session/camera-not-ready';
export type CaptureError =
| "capture/invalid-photo-format"
| "capture/encoder-error"
| "capture/muxer-error"
| "capture/recording-in-progress"
| "capture/no-recording-in-progress"
| "capture/file-io-error"
| "capture/create-temp-file-error"
| "capture/invalid-photo-codec"
| "capture/not-bound-error"
| "capture/capture-type-not-supported"
| "capture/unknown";
export type SystemError = "system/no-camera-manager";
export type UnknownError = "unknown/unknown";
| 'capture/invalid-photo-format'
| 'capture/encoder-error'
| 'capture/muxer-error'
| 'capture/recording-in-progress'
| 'capture/no-recording-in-progress'
| 'capture/file-io-error'
| 'capture/create-temp-file-error'
| 'capture/invalid-photo-codec'
| 'capture/not-bound-error'
| 'capture/capture-type-not-supported'
| 'capture/unknown';
export type SystemError = 'system/no-camera-manager';
export type UnknownError = 'unknown/unknown';
export interface ErrorWithCause {
/**
@ -69,15 +67,7 @@ export interface ErrorWithCause {
cause?: ErrorWithCause;
}
type CameraErrorCode =
| PermissionError
| ParameterError
| DeviceError
| FormatError
| SessionError
| CaptureError
| SystemError
| UnknownError;
type CameraErrorCode = PermissionError | ParameterError | DeviceError | FormatError | SessionError | CaptureError | SystemError | UnknownError;
/**
* Represents any kind of error that occured in the Camera View Module.
@ -98,7 +88,7 @@ class CameraError<TCode extends CameraErrorCode> extends Error {
}
constructor(code: TCode, message: string, cause?: ErrorWithCause) {
super(`[${code}]: ${message}${cause ? ` (Cause: ${cause.message})` : ""}`);
super(`[${code}]: ${message}${cause ? ` (Cause: ${cause.message})` : ''}`);
this._code = code;
this._message = message;
this._cause = cause;
@ -114,58 +104,44 @@ export class CameraCaptureError extends CameraError<CaptureError> {}
* Represents any kind of error that occured in the Camera View Module.
*/
export class CameraRuntimeError extends CameraError<
| PermissionError
| ParameterError
| DeviceError
| FormatError
| SessionError
| SystemError
| UnknownError
PermissionError | ParameterError | DeviceError | FormatError | SessionError | SystemError | UnknownError
> {}
export const isErrorWithCause = (error: unknown): error is ErrorWithCause =>
typeof error === "object" &&
typeof error === 'object' &&
error != null &&
// @ts-expect-error error is still unknown
typeof error.message === "string" &&
typeof error.message === 'string' &&
// @ts-expect-error error is still unknown
(typeof error.stacktrace === "string" || error.stacktrace == null) &&
(typeof error.stacktrace === 'string' || error.stacktrace == null) &&
// @ts-expect-error error is still unknown
(isErrorWithCause(error.cause) || error.cause == null);
const isCameraErrorJson = (
error: unknown
): error is { code: string; message: string; cause?: ErrorWithCause } =>
typeof error === "object" &&
const isCameraErrorJson = (error: unknown): error is { code: string; message: string; cause?: ErrorWithCause } =>
typeof error === 'object' &&
error != null &&
// @ts-expect-error error is still unknown
typeof error.code === "string" &&
typeof error.code === 'string' &&
// @ts-expect-error error is still unknown
typeof error.message === "string" &&
typeof error.message === 'string' &&
// @ts-expect-error error is still unknown
(typeof error.cause === "object" || error.cause == null);
(typeof error.cause === 'object' || error.cause == null);
/**
* Tries to parse an error coming from native to a typed JS camera error.
* @param nativeError The native error instance. This is a JSON in the legacy native module architecture.
* @returns A `CameraRuntimeError` or `CameraCaptureError`, or the nativeError if it's not parsable
*/
export const tryParseNativeCameraError = <T>(
nativeError: T
): (CameraRuntimeError | CameraCaptureError) | T => {
export const tryParseNativeCameraError = <T>(nativeError: T): (CameraRuntimeError | CameraCaptureError) | T => {
if (isCameraErrorJson(nativeError)) {
if (nativeError.code.startsWith("capture")) {
return new CameraCaptureError(
nativeError.code as CaptureError,
nativeError.message,
nativeError.cause
);
if (nativeError.code.startsWith('capture')) {
return new CameraCaptureError(nativeError.code as CaptureError, nativeError.message, nativeError.cause);
} else {
return new CameraRuntimeError(
// @ts-expect-error the code is string, we narrow it down to TS union.
nativeError.code,
nativeError.message,
nativeError.cause
nativeError.cause,
);
}
} else {

View File

@ -10,4 +10,4 @@
* #### Android only
* * `"external"`: The camera device is an external camera, and has no fixed facing relative to the device's screen. (Android only)
*/
export type CameraPosition = "front" | "back" | "unspecified" | "external";
export type CameraPosition = 'front' | 'back' | 'unspecified' | 'external';

View File

@ -15,15 +15,15 @@
* * `"vga-640x480"`: Specifies capture settings suitable for VGA quality (640 x 480 pixel) video output.
*/
export type CameraPreset =
| "cif-352x288"
| "hd-1280x720"
| "hd-1920x1080"
| "hd-3840x2160"
| "high"
| "iframe-1280x720"
| "iframe-960x540"
| "input-priority"
| "low"
| "medium"
| "photo"
| "vga-640x480";
| 'cif-352x288'
| 'hd-1280x720'
| 'hd-1920x1080'
| 'hd-3840x2160'
| 'high'
| 'iframe-1280x720'
| 'iframe-960x540'
| 'input-priority'
| 'low'
| 'medium'
| 'photo'
| 'vga-640x480';

View File

@ -2,24 +2,24 @@
* Available code types
*/
export type CodeType =
| "cat-body"
| "dog-body"
| "human-body"
| "salient-object"
| "aztec"
| "code-128"
| "code-39"
| "code-39-mod-43"
| "code-93"
| "data-matrix"
| "ean-13"
| "ean-8"
| "face"
| "interleaved-2-of-5"
| "itf-14"
| "pdf-417"
| "qr"
| "upce";
| 'cat-body'
| 'dog-body'
| 'human-body'
| 'salient-object'
| 'aztec'
| 'code-128'
| 'code-39'
| 'code-39-mod-43'
| 'code-93'
| 'data-matrix'
| 'ean-13'
| 'ean-8'
| 'face'
| 'interleaved-2-of-5'
| 'itf-14'
| 'pdf-417'
| 'qr'
| 'upce';
/**
* Represents a File in the local filesystem.

View File

@ -1,5 +1,5 @@
import type { CameraPhotoCodec } from "./CameraCodec";
import type { TemporaryFile } from "./TemporaryFile";
import type { CameraPhotoCodec } from './CameraCodec';
import type { TemporaryFile } from './TemporaryFile';
export interface TakePhotoOptions {
/**
@ -18,13 +18,13 @@ export interface TakePhotoOptions {
* @platform iOS 13.0+
* @default "balanced"
*/
qualityPrioritization?: "quality" | "balanced" | "speed";
qualityPrioritization?: 'quality' | 'balanced' | 'speed';
/**
* Whether the Flash should be enabled or disabled
*
* @default "auto"
*/
flash?: "on" | "off" | "auto";
flash?: 'on' | 'off' | 'auto';
/**
* Specifies whether red-eye reduction should be applied automatically on flash captures.
*
@ -86,8 +86,8 @@ export type PhotoFile = Readonly<
*
* @platform iOS
*/
"{MakerApple}"?: Record<string, unknown>;
"{TIFF}": {
'{MakerApple}'?: Record<string, unknown>;
'{TIFF}': {
ResolutionUnit: number;
Software: string;
Make: string;
@ -100,7 +100,7 @@ export type PhotoFile = Readonly<
Model: string;
YResolution: number;
};
"{Exif}": {
'{Exif}': {
DateTimeOriginal: string;
ExposureTime: number;
FNumber: number;

View File

@ -26,14 +26,14 @@
// maxKeyFrameIntervalDuration?: TCodec extends "h264" ? number : never;
// }
import type { CameraCaptureError } from "./CameraError";
import type { TemporaryFile } from "./TemporaryFile";
import type { CameraCaptureError } from './CameraError';
import type { TemporaryFile } from './TemporaryFile';
export interface RecordVideoOptions {
/**
* Set the video flash mode. Natively, this just enables the torch while recording.
*/
flash?: "on" | "off" | "auto";
flash?: 'on' | 'off' | 'auto';
/**
* Called when there was an unexpected runtime error while recording the video.
*/