ESLint autofix
This commit is contained in:
parent
50509200aa
commit
dc2be934f6
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
36
src/Code.ts
36
src/Code.ts
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user