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

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

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

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

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

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

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,