AI Camera gallery button (#1896)

* Add button to import photos from the gallery to the AI Camera
* Move AI Camera buttons to side rails so it's easier to add new buttons
* Got shutter button closer to spec
* Change e2e to test for element absence instead of status text

Closes #1848
This commit is contained in:
Ken-ichi
2024-07-31 11:14:53 -07:00
committed by GitHub
parent fc4b3caba9
commit 1679f2f24a
8 changed files with 143 additions and 75 deletions

View File

@@ -179,10 +179,13 @@ describe( "Signed in user", () => {
/ 5. Delete the observations
*/
await deleteObservationByUUID( uuid, username, { uploaded: true } );
// TODO test to make sure the exact observation we created was deleted.
// Testing for the empty list UI isn't adequate because other test runs
// happening in parallel might cause other observations to be there
const deletedObservationText = element( by.text( /1 observation deleted/ ) );
await waitFor( deletedObservationText ).toBeVisible().withTimeout( 10000 );
// It would be nice to test for the "1 observation deleted" status text in
// the toolbar, but that message appears ephemerally and sometimes this
// test doesn't pick it up on the Github runner. Since we created two
// observations, the upload prompt will be the stable status text at this
// point, and we can confirm deletion by testing for the absence of the
// list item for the observation we deleted.
await waitFor( element( by.text( /Upload 1 observation/ ) ) ).toBeVisible( ).withTimeout( 10000 );
await expect( obsListItem ).toBeNotVisible( );
} );
} );

View File

@@ -1,6 +1,7 @@
import CameraFlip from "components/Camera/Buttons/CameraFlip.tsx";
import Close from "components/Camera/Buttons/Close.tsx";
import Flash from "components/Camera/Buttons/Flash.tsx";
import Gallery from "components/Camera/Buttons/Gallery.tsx";
import TakePhoto from "components/Camera/Buttons/TakePhoto.tsx";
import Zoom from "components/Camera/Buttons/Zoom.tsx";
import TabletButtons from "components/Camera/TabletButtons.tsx";
@@ -67,6 +68,7 @@ const AICameraButtons = ( {
disabled={!modelLoaded || takingPhoto}
flipCamera={flipCamera}
hasFlash={hasFlash}
hasGalleryButton
rotatableAnimatedStyle={rotatableAnimatedStyle}
showPrediction={showPrediction}
showZoomButton={showZoomButton}
@@ -78,48 +80,61 @@ const AICameraButtons = ( {
);
}
return (
<View className="bottom-10 absolute right-5 left-5">
<View className="flex-row justify-end pb-[30px]">
<Zoom
zoomTextValue={zoomTextValue}
changeZoom={changeZoom}
showZoomButton={showZoomButton}
rotatableAnimatedStyle={rotatableAnimatedStyle}
/>
<View className="bottom-10 absolute right-5 left-5" pointerEvents="box-none">
<View
className="absolute left-0 bottom-[17px] h-full justify-end flex gap-y-9"
pointerEvents="box-none"
>
<View><Close /></View>
</View>
<AIDebugButton
confidenceThreshold={confidenceThreshold}
setConfidenceThreshold={setConfidenceThreshold}
fps={fps}
setFPS={setFPS}
numStoredResults={numStoredResults}
setNumStoredResults={setNumStoredResults}
cropRatio={cropRatio}
setCropRatio={setCropRatio}
// TODO: The following are just to get accessibility tests to pass...
// without making anything truly accessible. The test seems to think
// AIDebugButton is itself not accessible, but it's really
// complaining about the sliders within. If the sliders make it into
// production, they'll need to be made to pass that test.
accessibilityRole="adjustable"
accessibilityValue={{ min: 0, max: 100, now: 50 }}
/>
<View className="flex-row justify-end pb-[20px]">
<Flash
toggleFlash={toggleFlash}
hasFlash={hasFlash}
takePhotoOptions={takePhotoOptions}
rotatableAnimatedStyle={rotatableAnimatedStyle}
<View
className="absolute right-0 bottom-[6px] h-full justify-end items-end flex gap-y-9"
pointerEvents="box-none"
>
<AIDebugButton
confidenceThreshold={confidenceThreshold}
setConfidenceThreshold={setConfidenceThreshold}
fps={fps}
setFPS={setFPS}
numStoredResults={numStoredResults}
setNumStoredResults={setNumStoredResults}
cropRatio={cropRatio}
setCropRatio={setCropRatio}
// TODO: The following are just to get accessibility tests to pass...
// without making anything truly accessible. The test seems to think
// AIDebugButton is itself not accessible, but it's really
// complaining about the sliders within. If the sliders make it into
// production, they'll need to be made to pass that test.
accessibilityRole="adjustable"
accessibilityValue={{ min: 0, max: 100, now: 50 }}
/>
<View>
<Zoom
zoomTextValue={zoomTextValue}
changeZoom={changeZoom}
showZoomButton={showZoomButton}
rotatableAnimatedStyle={rotatableAnimatedStyle}
/>
</View>
<View>
<Flash
toggleFlash={toggleFlash}
hasFlash={hasFlash}
takePhotoOptions={takePhotoOptions}
rotatableAnimatedStyle={rotatableAnimatedStyle}
/>
</View>
<View><CameraFlip flipCamera={flipCamera} /></View>
<View>
<Gallery rotatableAnimatedStyle={rotatableAnimatedStyle} />
</View>
</View>
<View className="flex-row justify-between items-center">
<Close />
<View className="flex-row justify-center items-center w-full" pointerEvents="box-none">
<TakePhoto
disabled={!modelLoaded || takingPhoto}
takePhoto={takePhoto}
showPrediction={showPrediction}
/>
<CameraFlip flipCamera={flipCamera} />
</View>
</View>
);

View File

@@ -66,7 +66,7 @@ const AIDebugButton = ( {
if ( !isDebug ) return null;
return (
<View className="flex-row justify-end pb-[20px]">
<View className="flex-row justify-end">
<INatIconButton
className={classnames(
"bg-deeppink",

View File

@@ -1,8 +1,6 @@
import classnames from "classnames";
import TransparentCircleButton, {
CIRCLE_SIZE
} from "components/SharedComponents/Buttons/TransparentCircleButton.tsx";
import { View } from "components/styledComponents";
// eslint-disable-next-line max-len
import TransparentCircleButton from "components/SharedComponents/Buttons/TransparentCircleButton.tsx";
import React from "react";
import { GestureResponderEvent, ViewStyle } from "react-native";
import DeviceInfo from "react-native-device-info";
@@ -15,21 +13,11 @@ const isTablet = DeviceInfo.isTablet();
interface Props {
rotatableAnimatedStyle: ViewStyle;
toggleFlash: ( _event: GestureResponderEvent ) => void;
hasFlash: boolean;
hasFlash?: boolean;
takePhotoOptions: TakePhotoOptions;
flashClassName?: string;
}
// Empty space where a camera button should be so buttons don't jump around
// when they appear or disappear
const CameraButtonPlaceholder = ( ) => (
<View
accessibilityElementsHidden
aria-hidden
className={CIRCLE_SIZE}
/>
);
const Flash = ( {
rotatableAnimatedStyle,
toggleFlash,
@@ -39,7 +27,7 @@ const Flash = ( {
}: Props ) => {
const { t } = useTranslation( );
if ( !hasFlash ) return <CameraButtonPlaceholder />;
if ( !hasFlash ) return null;
let testID = "";
let accessibilityHint = "";
let name = "";
@@ -59,9 +47,9 @@ const Flash = ( {
<Animated.View
style={!isTablet && rotatableAnimatedStyle}
className={classnames(
flashClassName,
"m-0",
"border-0"
"border-0",
flashClassName
)}
>
<TransparentCircleButton

View File

@@ -0,0 +1,48 @@
import { useNavigation } from "@react-navigation/native";
import classnames from "classnames";
import { INatIconButton } from "components/SharedComponents";
import React from "react";
import { ViewStyle } from "react-native";
import DeviceInfo from "react-native-device-info";
import Animated from "react-native-reanimated";
import { useTranslation } from "sharedHooks";
import colors from "styles/tailwindColors";
interface Props {
rotatableAnimatedStyle: ViewStyle;
}
const isTablet = DeviceInfo.isTablet();
const Gallery = ( { rotatableAnimatedStyle }: Props ) => {
const { t } = useTranslation( );
const navigation = useNavigation( );
return (
<Animated.View
style={!isTablet && rotatableAnimatedStyle}
className="m-0 border-0"
>
<INatIconButton
className={classnames(
"bg-black/50",
"items-center",
"justify-center",
"border-white",
"border-2",
"rounded"
)}
onPress={( ) => navigation.push( "PhotoGallery", { cmonBack: true } )}
accessibilityLabel={t( "Photo-importer" )}
accessibilityHint={t( "Navigates-to-photo-importer" )}
icon="gallery"
color={( colors?.white as string ) || "white"}
size={26}
width={62}
height={62}
/>
</Animated.View>
);
};
export default Gallery;

View File

@@ -11,7 +11,7 @@ import { useTranslation } from "sharedHooks";
import { getShadowForColor } from "styles/global";
import colors from "styles/tailwindColors";
const DROP_SHADOW = getShadowForColor( colors.darkGray );
const DROP_SHADOW = getShadowForColor( colors.black, { offsetHeight: 4 } );
interface Props {
takePhoto: () => Promise<void>;
@@ -27,7 +27,7 @@ const TakePhoto = ( {
const { t } = useTranslation( );
const theme = useTheme( );
const borderClass = "border-[1.64px] rounded-full h-[60px] w-[60px]";
const borderClass = "border-[2px] rounded-full h-[64px] w-[64px]";
return (
<Pressable
@@ -47,19 +47,19 @@ const TakePhoto = ( {
accessibilityRole="button"
accessibilityState={{ disabled }}
disabled={disabled}
style={DROP_SHADOW}
>
{showPrediction
? (
<View
style={DROP_SHADOW}
className={classnames(
borderClass,
"bg-inatGreen items-center justify-center border-darkGray"
"bg-inatGreen items-center justify-center border-accessibleGreen"
)}
>
<INatIcon
name="sparkly-label"
size={24}
size={32}
color={theme.colors.onPrimary}
/>
</View>

View File

@@ -1,4 +1,5 @@
import classnames from "classnames";
import Gallery from "components/Camera/Buttons/Gallery.tsx";
import { CloseButton } from "components/SharedComponents";
import { View } from "components/styledComponents";
import React from "react";
@@ -41,7 +42,8 @@ interface Props {
flipCamera: ( _event: GestureResponderEvent ) => void;
handleCheckmarkPress?: ( _event: GestureResponderEvent ) => void;
handleClose?: ( _event: GestureResponderEvent ) => void;
hasFlash: boolean;
hasFlash?: boolean;
hasGalleryButton?: boolean;
photosTaken?: boolean;
rotatableAnimatedStyle: ViewStyle;
showPrediction?: boolean;
@@ -54,13 +56,15 @@ interface Props {
// Empty space where a camera button should be so buttons don't jump around
// when they appear or disappear
const CameraButtonPlaceholder = ( ) => (
const CameraButtonPlaceholder = ( { extraClassName }: { extraClassName?: string } ) => (
<View
accessibilityElementsHidden
aria-hidden
className={classnames(
// "bg-deeppink",
`w-[${CAMERA_BUTTON_DIM}px]`,
`h-[${CAMERA_BUTTON_DIM}px]`
`h-[${CAMERA_BUTTON_DIM}px]`,
extraClassName
)}
/>
);
@@ -72,6 +76,7 @@ const TabletButtons = ( {
handleCheckmarkPress,
handleClose,
hasFlash,
hasGalleryButton,
photosTaken,
rotatableAnimatedStyle,
showPrediction,
@@ -83,33 +88,34 @@ const TabletButtons = ( {
}: Props ) => {
const tabletCameraOptionsClasses = [
"absolute",
"h-[380px]",
"items-center",
"justify-center",
"mr-5",
"mt-[-190px]",
"pb-0",
"p-0",
"right-0",
"top-[50%]"
"h-full"
];
return (
<View className={classnames( tabletCameraOptionsClasses )}>
<View className={classnames( tabletCameraOptionsClasses )} pointerEvents="box-none">
{ photosTaken && <CameraButtonPlaceholder extraClassName="mb-[25px]" /> }
<Zoom
changeZoom={changeZoom}
zoomTextValue={zoomTextValue}
showZoomButton={showZoomButton}
rotatableAnimatedStyle={rotatableAnimatedStyle}
zoomClassName="mb-[25px]"
/>
<Flash
toggleFlash={toggleFlash}
hasFlash={hasFlash}
takePhotoOptions={takePhotoOptions}
rotatableAnimatedStyle={rotatableAnimatedStyle}
flashClassName="m-0 mb-[25px]"
/>
<CameraFlip
flipCamera={flipCamera}
cameraFlipClasses="m-0 mt-[25px]"
cameraFlipClasses="m-0"
/>
<View className="mt-[40px] mb-[40px]">
<TakePhoto
@@ -121,7 +127,7 @@ const TabletButtons = ( {
{ photosTaken && (
<Animated.View
style={!isTablet && rotatableAnimatedStyle}
className={classnames( checkmarkClasses, "mb-[25px]" )}
className={classnames( checkmarkClasses )}
>
<GreenCheckmark
handleCheckmarkPress={handleCheckmarkPress || ( () => null )}
@@ -131,7 +137,7 @@ const TabletButtons = ( {
<View
className={classnames(
cameraOptionsClasses,
{ "mb-[25px]": !photosTaken }
{ "mt-[25px]": photosTaken }
)}
>
<CloseButton
@@ -139,7 +145,13 @@ const TabletButtons = ( {
size={18}
/>
</View>
{ !photosTaken && <CameraButtonPlaceholder /> }
{ hasFlash && <CameraButtonPlaceholder extraClassName="mt-[25px]" /> }
{ showZoomButton && <CameraButtonPlaceholder extraClassName="mt-[25px]" /> }
{ hasGalleryButton && (
<View className="absolute bottom-6">
<Gallery rotatableAnimatedStyle={rotatableAnimatedStyle} />
</View>
) }
</View>
);
};

View File

@@ -123,6 +123,8 @@ const PhotoGallery = ( ): Node => {
// Determine if we need to go back to ObsList or ObsDetails screen
} else if ( params && params.previousScreen && params.previousScreen.name === "ObsDetails" ) {
navigateToObsDetails( navigation, params.previousScreen.params.uuid );
} else if ( params?.cmonBack && navigation.canGoBack() ) {
navigation.goBack();
} else {
navToObsList();
}