chore: Cleanup codebase (#137)
* Remove `useCachedState` * Add pressable opacity * Update Media.tsx * f * Update FormatFilter.ts * update * App -> CameraPage, Media -> MediaPage * Update CameraPage.tsx * Create 60 FPS switch * Update CameraPage.tsx
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useRef, useState, useMemo, useCallback } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import {
|
||||
PinchGestureHandler,
|
||||
PinchGestureHandlerGestureEvent,
|
||||
@@ -11,18 +11,16 @@ import {
|
||||
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
||||
import type { CameraDeviceFormat, CameraRuntimeError, PhotoFile, VideoFile } from 'react-native-vision-camera';
|
||||
import { Camera, frameRateIncluded, sortFormatsByResolution, filterFormatsByAspectRatio } from 'react-native-vision-camera';
|
||||
import { useIsScreenFocused } from './hooks/useIsScreenFocused';
|
||||
import { useIsScreenFocussed } from './hooks/useIsScreenFocused';
|
||||
import { CONTENT_SPACING, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING } from './Constants';
|
||||
import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from 'react-native-reanimated';
|
||||
import { useEffect } from 'react';
|
||||
import { useIsForeground } from './hooks/useIsForeground';
|
||||
import { StatusBarBlurBackground } from './views/StatusBarBlurBackground';
|
||||
import { CaptureButton } from './views/CaptureButton';
|
||||
import { PressableOpacity } from './views/PressableOpacity';
|
||||
import { PressableOpacity } from 'react-native-pressable-opacity';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
import { useSelector } from 'pipestate';
|
||||
import { FpsSelector } from './state/selectors';
|
||||
import { useCameraDevice } from './hooks/useCameraDevice';
|
||||
|
||||
const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera);
|
||||
@@ -33,14 +31,14 @@ Reanimated.addWhitelistedNativeProps({
|
||||
const SCALE_FULL_ZOOM = 3;
|
||||
const BUTTON_SIZE = 40;
|
||||
|
||||
export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
||||
const camera = useRef<Camera>(null);
|
||||
const [isCameraInitialized, setIsCameraInitialized] = useState(false);
|
||||
const zoom = useSharedValue(0);
|
||||
const isPressingButton = useSharedValue(false);
|
||||
|
||||
// check if camera page is active
|
||||
const isFocussed = useIsScreenFocused(componentId);
|
||||
const isFocussed = useIsScreenFocussed(componentId);
|
||||
const isForeground = useIsForeground();
|
||||
const isActive = isFocussed && isForeground;
|
||||
|
||||
@@ -59,32 +57,34 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
}, [device?.formats]);
|
||||
|
||||
//#region Memos
|
||||
const [targetFps] = useSelector(FpsSelector);
|
||||
console.log(`Target FPS: ${targetFps}`);
|
||||
const [is60Fps, setIs60Fps] = useState(true);
|
||||
const fps = useMemo(() => {
|
||||
if (!is60Fps) return 30;
|
||||
|
||||
if (enableNightMode && !device?.supportsLowLightBoost) {
|
||||
// User has enabled Night Mode, but Night Mode is not natively supported, so we simulate it by lowering the frame rate.
|
||||
return 30;
|
||||
}
|
||||
|
||||
const supportsHdrAtHighFps = formats.some((f) => f.supportsVideoHDR && f.frameRateRanges.some((r) => frameRateIncluded(r, targetFps)));
|
||||
if (enableHdr && !supportsHdrAtHighFps) {
|
||||
// User has enabled HDR, but HDR is not supported at targetFps.
|
||||
const supportsHdrAt60Fps = formats.some((f) => f.supportsVideoHDR && f.frameRateRanges.some((r) => frameRateIncluded(r, 60)));
|
||||
if (enableHdr && !supportsHdrAt60Fps) {
|
||||
// User has enabled HDR, but HDR is not supported at 60 FPS.
|
||||
return 30;
|
||||
}
|
||||
|
||||
const supportsHighFps = formats.some((f) => f.frameRateRanges.some((r) => frameRateIncluded(r, targetFps)));
|
||||
if (!supportsHighFps) {
|
||||
// targetFps is not supported by any format.
|
||||
const supports60Fps = formats.some((f) => f.frameRateRanges.some((r) => frameRateIncluded(r, 60)));
|
||||
if (!supports60Fps) {
|
||||
// 60 FPS is not supported by any format.
|
||||
return 30;
|
||||
}
|
||||
// If nothing blocks us from using it, we default to targetFps.
|
||||
return targetFps;
|
||||
}, [device?.supportsLowLightBoost, enableHdr, enableNightMode, formats, targetFps]);
|
||||
// If nothing blocks us from using it, we default to 60 FPS.
|
||||
return 60;
|
||||
}, [device?.supportsLowLightBoost, enableHdr, enableNightMode, formats, is60Fps]);
|
||||
|
||||
const supportsCameraFlipping = useMemo(() => devices.back != null && devices.front != null, [devices.back, devices.front]);
|
||||
const supportsFlash = device?.hasFlash ?? false;
|
||||
const supportsHdr = useMemo(() => formats.some((f) => f.supportsVideoHDR), [formats]);
|
||||
const supports60Fps = useMemo(() => formats.some((f) => f.frameRateRanges.some((rate) => frameRateIncluded(rate, 60))), [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
|
||||
@@ -136,7 +136,7 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
console.log(`Media captured! ${JSON.stringify(media)}`);
|
||||
await Navigation.showModal({
|
||||
component: {
|
||||
name: 'Media',
|
||||
name: 'MediaPage',
|
||||
passProps: {
|
||||
type: type,
|
||||
path: media.path,
|
||||
@@ -147,18 +147,9 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
const onFlipCameraPressed = useCallback(() => {
|
||||
setCameraPosition((p) => (p === 'back' ? 'front' : 'back'));
|
||||
}, []);
|
||||
const onHdrSwitchPressed = useCallback(() => {
|
||||
setEnableHdr((h) => !h);
|
||||
}, []);
|
||||
const onFlashPressed = useCallback(() => {
|
||||
setFlash((f) => (f === 'off' ? 'on' : 'off'));
|
||||
}, []);
|
||||
const onNightModePressed = useCallback(() => {
|
||||
setEnableNightMode((n) => !n);
|
||||
}, []);
|
||||
const onSettingsPressed = useCallback(() => {
|
||||
Navigation.push(componentId, { component: { name: 'Settings' } });
|
||||
}, [componentId]);
|
||||
//#endregion
|
||||
|
||||
//#region Tap Gesture
|
||||
@@ -269,19 +260,24 @@ export const App: NavigationFunctionComponent = ({ componentId }) => {
|
||||
<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} />
|
||||
{supports60Fps && (
|
||||
<PressableOpacity style={styles.button} onPress={() => setIs60Fps(!is60Fps)}>
|
||||
<Text style={styles.text}>
|
||||
{is60Fps ? '60' : '30'}
|
||||
{'\n'}FPS
|
||||
</Text>
|
||||
</PressableOpacity>
|
||||
)}
|
||||
{supportsHdr && (
|
||||
<PressableOpacity style={styles.button} onPress={onHdrSwitchPressed}>
|
||||
<PressableOpacity style={styles.button} onPress={() => setEnableHdr((h) => !h)}>
|
||||
<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} />
|
||||
</PressableOpacity>
|
||||
{canToggleNightMode && (
|
||||
<PressableOpacity style={styles.button} onPress={() => setEnableNightMode(!enableNightMode)} disabledOpacity={0.4}>
|
||||
<IonIcon name={enableNightMode ? 'moon' : 'moon-outline'} color="white" size={24} />
|
||||
</PressableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -311,4 +307,10 @@ const styles = StyleSheet.create({
|
||||
right: SAFE_AREA_PADDING.paddingRight,
|
||||
top: SAFE_AREA_PADDING.paddingTop,
|
||||
},
|
||||
text: {
|
||||
color: 'white',
|
||||
fontSize: 11,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
@@ -4,8 +4,8 @@ import { Navigation, NavigationFunctionComponent, OptionsModalPresentationStyle
|
||||
import Video, { LoadError, OnLoadData } from 'react-native-video';
|
||||
import { SAFE_AREA_PADDING } from './Constants';
|
||||
import { useIsForeground } from './hooks/useIsForeground';
|
||||
import { useIsScreenFocused } from './hooks/useIsScreenFocused';
|
||||
import { PressableOpacity } from './views/PressableOpacity';
|
||||
import { useIsScreenFocussed } from './hooks/useIsScreenFocused';
|
||||
import { PressableOpacity } from 'react-native-pressable-opacity';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
import { Alert } from 'react-native';
|
||||
import CameraRoll from '@react-native-community/cameraroll';
|
||||
@@ -34,10 +34,10 @@ const requestSavePermission = async (): Promise<boolean> => {
|
||||
const isVideoOnLoadEvent = (event: OnLoadData | NativeSyntheticEvent<ImageLoadEventData>): event is OnLoadData =>
|
||||
'duration' in event && 'naturalSize' in event;
|
||||
|
||||
export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, type, path }) => {
|
||||
export const MediaPage: NavigationFunctionComponent<MediaProps> = ({ componentId, type, path }) => {
|
||||
const [hasMediaLoaded, setHasMediaLoaded] = useState(false);
|
||||
const isForeground = useIsForeground();
|
||||
const isScreenFocused = useIsScreenFocused(componentId);
|
||||
const isScreenFocused = useIsScreenFocussed(componentId);
|
||||
const isVideoPaused = !isForeground || !isScreenFocused;
|
||||
const [savingState, setSavingState] = useState<'none' | 'saving' | 'saved'>('none');
|
||||
|
||||
@@ -126,7 +126,7 @@ export const Media: NavigationFunctionComponent<MediaProps> = ({ componentId, ty
|
||||
);
|
||||
};
|
||||
|
||||
Media.options = {
|
||||
MediaPage.options = {
|
||||
modal: {
|
||||
swipeToDismiss: false,
|
||||
},
|
@@ -1,110 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { StyleSheet, View, Text, Linking } from 'react-native';
|
||||
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import { useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Camera } from 'react-native-vision-camera';
|
||||
import { SAFE_AREA_PADDING } from './Constants';
|
||||
import { useSelector } from 'pipestate';
|
||||
import { FpsSelector } from './state/selectors';
|
||||
|
||||
export const Settings: NavigationFunctionComponent = ({ componentId }) => {
|
||||
const [fpsSelector, setFpsSelector] = useSelector(FpsSelector);
|
||||
const [fps, setFps] = useState(fpsSelector);
|
||||
|
||||
const [minFps, setMinFps] = useState<number>();
|
||||
const [maxFps, setMaxFps] = useState<number>();
|
||||
|
||||
const onCuventPressed = useCallback(() => {
|
||||
Linking.openURL('https://cuvent.com');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadFormats = async (): Promise<void> => {
|
||||
const devices = await Camera.getAvailableCameraDevices();
|
||||
const formats = devices.flatMap((d) => d.formats);
|
||||
let max = 0,
|
||||
min = 0;
|
||||
formats.forEach((format) => {
|
||||
const frameRates = format.frameRateRanges.map((f) => f.maxFrameRate).sort((left, right) => left - right);
|
||||
const highest = frameRates[frameRates.length - 1] as number;
|
||||
const lowest = frameRates[0] as number;
|
||||
if (highest > max) max = highest;
|
||||
if (lowest < min) min = lowest;
|
||||
});
|
||||
setMaxFps(max);
|
||||
setMinFps(min);
|
||||
};
|
||||
loadFormats();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = Navigation.events().registerScreenPoppedListener((event) => {
|
||||
if (event.componentId === componentId) setFpsSelector(Math.round(fps));
|
||||
});
|
||||
return () => {
|
||||
listener.remove();
|
||||
};
|
||||
}, [componentId, fps, setFpsSelector]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.vControl}>
|
||||
<Text>Frame Rate (FPS): {Math.round(fps)}</Text>
|
||||
{minFps != null && maxFps != null && <Slider minimumValue={minFps} maximumValue={maxFps} value={fps} onValueChange={setFps} />}
|
||||
</View>
|
||||
|
||||
<View style={styles.spacer} />
|
||||
|
||||
<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',
|
||||
},
|
||||
backButton: {
|
||||
id: 'back',
|
||||
showTitle: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'stretch',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'white',
|
||||
...SAFE_AREA_PADDING,
|
||||
},
|
||||
aboutText: {
|
||||
alignSelf: 'center',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: '#A9A9A9',
|
||||
maxWidth: '50%',
|
||||
textAlign: 'center',
|
||||
},
|
||||
hyperlink: {
|
||||
color: '#007aff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
vControl: {
|
||||
width: '100%',
|
||||
},
|
||||
spacer: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
@@ -1,26 +0,0 @@
|
||||
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] => {
|
||||
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);
|
||||
if (areEqual) {
|
||||
return;
|
||||
} else {
|
||||
cachedState.current = newState;
|
||||
setState(newState);
|
||||
}
|
||||
},
|
||||
[comparator],
|
||||
);
|
||||
|
||||
return [state, dispatchState];
|
||||
};
|
@@ -1,9 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { AppState, AppStateStatus } from 'react-native';
|
||||
import { useCachedState } from './useCachedState';
|
||||
|
||||
export const useIsForeground = (): boolean => {
|
||||
const [isForeground, setIsForeground] = useCachedState(true);
|
||||
const [isForeground, setIsForeground] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (state: AppStateStatus): void => {
|
||||
|
@@ -1,57 +1,39 @@
|
||||
import { useEffect, useMemo, useReducer } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Navigation } from 'react-native-navigation';
|
||||
|
||||
type Action =
|
||||
| {
|
||||
action: 'push';
|
||||
componentId: string;
|
||||
}
|
||||
| {
|
||||
action: 'pop';
|
||||
componentId: string;
|
||||
};
|
||||
export const useIsScreenFocussed = (componentId: string): boolean => {
|
||||
const componentStack = useRef<string[]>(['componentId']);
|
||||
const [isFocussed, setIsFocussed] = useState(true);
|
||||
|
||||
const reducer = (stack: string[], action: Action): string[] => {
|
||||
switch (action.action) {
|
||||
case 'push': {
|
||||
stack.push(action.componentId);
|
||||
break;
|
||||
}
|
||||
case 'pop': {
|
||||
const index = stack.indexOf(action.componentId);
|
||||
if (index > -1) stack.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [...stack];
|
||||
};
|
||||
|
||||
export const useIsScreenFocused = (componentId: string): boolean => {
|
||||
const [componentStack, dispatch] = useReducer(reducer, [componentId]);
|
||||
const isFocussed = useMemo(() => componentStack[componentStack.length - 1] === componentId, [componentStack, componentId]);
|
||||
const invalidate = useCallback(() => {
|
||||
const last = componentStack.current[componentStack.current.length - 1];
|
||||
setIsFocussed(last === componentId);
|
||||
}, [componentId, setIsFocussed]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = Navigation.events().registerComponentDidAppearListener((event) => {
|
||||
if (event.componentType !== 'Component') return;
|
||||
dispatch({
|
||||
action: 'push',
|
||||
componentId: event.componentId,
|
||||
});
|
||||
componentStack.current.push(event.componentId);
|
||||
invalidate();
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
}, [invalidate]);
|
||||
useEffect(() => {
|
||||
const listener = Navigation.events().registerComponentDidDisappearListener((event) => {
|
||||
if (event.componentType !== 'Component') return;
|
||||
dispatch({
|
||||
action: 'pop',
|
||||
componentId: event.componentId,
|
||||
});
|
||||
// we can't simply use .pop() here because the component might be popped deeper down in the hierarchy.
|
||||
for (let i = componentStack.current.length - 1; i >= 0; i--) {
|
||||
if (componentStack.current[i] === event.componentId) {
|
||||
componentStack.current.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
invalidate();
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
}, [invalidate]);
|
||||
|
||||
return isFocussed;
|
||||
};
|
||||
|
@@ -1,11 +0,0 @@
|
||||
import { atom } from 'pipestate';
|
||||
|
||||
interface FormatSettings {
|
||||
fps: number;
|
||||
}
|
||||
|
||||
export const FormatSettingsAtom = atom<FormatSettings>({
|
||||
default: {
|
||||
fps: 60,
|
||||
},
|
||||
});
|
@@ -1,13 +0,0 @@
|
||||
import { selector } from 'pipestate';
|
||||
import { FormatSettingsAtom } from './atoms';
|
||||
|
||||
export const FpsSelector = selector<number, []>({
|
||||
get: ({ get }) => {
|
||||
return get(FormatSettingsAtom).fps;
|
||||
},
|
||||
set: ({ set, get }, newFps) => {
|
||||
const formatSettings = get(FormatSettingsAtom);
|
||||
set(FormatSettingsAtom, { ...formatSettings, fps: newFps });
|
||||
},
|
||||
dependencies: [FormatSettingsAtom],
|
||||
});
|
@@ -1,50 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { PressableProps, Pressable, PressableStateCallbackType, StyleProp, ViewStyle } from 'react-native';
|
||||
|
||||
export interface PressableOpacityProps extends PressableProps {
|
||||
/**
|
||||
* The opacity to use when `disabled={true}`
|
||||
*
|
||||
* @default 1
|
||||
*/
|
||||
disabledOpacity?: number;
|
||||
/**
|
||||
* The opacity to animate to when the user presses the button
|
||||
*
|
||||
* @default 0.2
|
||||
*/
|
||||
activeOpacity?: number;
|
||||
}
|
||||
|
||||
export type StyleType = (state: PressableStateCallbackType) => StyleProp<ViewStyle>;
|
||||
|
||||
/**
|
||||
* A Pressable component that lowers opacity when in pressed state. Uses the JS Pressability API.
|
||||
*/
|
||||
export const PressableOpacity = ({
|
||||
style,
|
||||
disabled = false,
|
||||
disabledOpacity = 1,
|
||||
activeOpacity = 0.2,
|
||||
...passThroughProps
|
||||
}: PressableOpacityProps): React.ReactElement => {
|
||||
const getOpacity = useCallback(
|
||||
(pressed: boolean) => {
|
||||
if (disabled) {
|
||||
return disabledOpacity;
|
||||
} else {
|
||||
if (pressed) return activeOpacity;
|
||||
else return 1;
|
||||
}
|
||||
},
|
||||
[activeOpacity, disabled, disabledOpacity],
|
||||
);
|
||||
const _style = useCallback<StyleType>(({ pressed }) => [style as ViewStyle, { opacity: getOpacity(pressed) }], [getOpacity, style]);
|
||||
|
||||
return <Pressable style={_style} disabled={disabled} {...passThroughProps} />;
|
||||
};
|
||||
|
||||
// Fallback implementation using TouchableOpacity:
|
||||
// export default function PressableOpacity(props: TouchableOpacityProps & { children?: React.ReactNode }): React.ReactElement {
|
||||
// return <TouchableOpacity delayPressIn={0} {...props} />;
|
||||
// }
|
Reference in New Issue
Block a user