From b04c6aa345266216933dbd3aa7f0f98ceec8b786 Mon Sep 17 00:00:00 2001 From: Loewy Date: Mon, 12 Feb 2024 10:28:42 -0800 Subject: [PATCH] render video detail inputs conditionally, terminateUpload on save working, needs testing wip: sync styles for inputs, remove notes/tags for now wip: use single screen and switch on mode need to rebase working, needs testing wip: sync styles for inputs, remove notes/tags for now wip: use single screen and switch on mode need to rebase working, needs testing add game type and table size to terminateUpload --- app.json | 2 +- package.json | 2 +- src/assets/{ => icons}/favicon.png | Bin src/component/video/camera.tsx | 25 ++- src/lib/alert-messages/constants.ts | 9 + src/lib/alert-messages/index.ts | 5 +- src/navigation/tab-navigator.tsx | 15 +- src/screens/video-stack/record.tsx | 161 ----------------- src/screens/video-stack/styles.ts | 33 +--- src/screens/video-stack/video-details.tsx | 179 +++++++++++++++++++ src/screens/video-stack/video-stack-types.ts | 6 + src/styles.ts | 29 +++ yarn.lock | 4 +- 13 files changed, 264 insertions(+), 206 deletions(-) rename src/assets/{ => icons}/favicon.png (100%) delete mode 100644 src/screens/video-stack/record.tsx create mode 100644 src/screens/video-stack/video-details.tsx create mode 100644 src/screens/video-stack/video-stack-types.ts diff --git a/app.json b/app.json index 9e0404e..e68d44e 100644 --- a/app.json +++ b/app.json @@ -38,7 +38,7 @@ "googleServicesFile": "./google-services.json" }, "web": { - "favicon": "./src/assets/favicon.png" + "favicon": "./src/assets/icons/favicon.png" } } } diff --git a/package.json b/package.json index c5b7722..cba73a1 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "graphql": "^16.8.1", "jest": "^29.2.1", "jest-expo": "~49.0.0", - "railbird-gql": "git+https://dev.railbird.ai/railbird/railbird-gql.git#master", + "railbird-gql": "git+https://dev.railbird.ai/railbird/railbird-gql.git#234d4d0fa90342f8af655c9ddf476f033caa322d", "react": "18.2.0", "react-native": "0.72.6", "react-native-dotenv": "^3.4.9", diff --git a/src/assets/favicon.png b/src/assets/icons/favicon.png similarity index 100% rename from src/assets/favicon.png rename to src/assets/icons/favicon.png diff --git a/src/component/video/camera.tsx b/src/component/video/camera.tsx index c1ed84b..8dbb6cb 100644 --- a/src/component/video/camera.tsx +++ b/src/component/video/camera.tsx @@ -4,7 +4,6 @@ import * as gql from "railbird-gql"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { Button, StyleSheet, Text, View } from "react-native"; import * as RNFS from "react-native-fs"; - import { Camera, CameraRuntimeError, @@ -19,6 +18,7 @@ import { RecordingButton } from "./capture-button"; import { useIsForeground } from "./is-foreground"; type Dictionary = { + // eslint-disable-next-line no-unused-vars [key in KeyType]: ValueType; }; @@ -111,6 +111,7 @@ export default function CameraScreen({ navigation, }): React.ReactElement { const apolloClient = useApolloClient(); + const { params } = route; const [createUpload, { data, loading, error }] = gql.useCreateUploadStreamMutation(); @@ -127,6 +128,7 @@ export default function CameraScreen({ console.log(`VideoId: ${newVideoId}`); setUploadManager(new StreamUploadManager(apolloClient, newVideoId)); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, uploadManager]); const camera = useRef(null); @@ -143,14 +145,24 @@ export default function CameraScreen({ }, []); const onInitialized = useCallback(() => { - console.log("Camera initialized!"); setIsCameraInitialized(true); - createUpload({ variables: { videoName: "Test" } }); + createUpload({ + variables: { videoName: params?.sessionName ?? "New session" }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const onMediaCaptured = useCallback((media: PhotoFile | VideoFile) => { - console.log(`Media captured! ${JSON.stringify(media)}`); - }, []); + const onMediaCaptured = useCallback( + (media: PhotoFile | VideoFile) => { + console.log(`Media captured! ${JSON.stringify(media)}`); + navigation.push("SaveVideo", { + videoId: uploadManager?.videoId, + ...params, + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [uploadManager, navigation, params], + ); const onVideoChunkReady = useCallback( (event) => { @@ -179,7 +191,6 @@ export default function CameraScreen({ // Replace with error handling if (device === null) { - console.log(device); // hasPermission redundant here - user should not be able to launch camera without permissions return ( diff --git a/src/lib/alert-messages/constants.ts b/src/lib/alert-messages/constants.ts index 02e5d94..45cb33a 100644 --- a/src/lib/alert-messages/constants.ts +++ b/src/lib/alert-messages/constants.ts @@ -8,6 +8,10 @@ interface PermissionMessage { message: string; }; } +interface ApiError { + title: string; + message: string; +} export const CAMERA_PERMISSION_DENIED: PermissionMessage = { android: { @@ -19,3 +23,8 @@ export const CAMERA_PERMISSION_DENIED: PermissionMessage = { message: "Please go to Settings > Railbird > Camera and grant permissions.", }, }; + +export const TERMINATE_UPLOAD_ERROR: ApiError = { + title: "There was an issue.", + message: "Please try again", +}; diff --git a/src/lib/alert-messages/index.ts b/src/lib/alert-messages/index.ts index 4c76604..6f4b839 100644 --- a/src/lib/alert-messages/index.ts +++ b/src/lib/alert-messages/index.ts @@ -1,12 +1,13 @@ import { Alert, Platform } from "react-native"; -import { CAMERA_PERMISSION_DENIED } from "./constants"; +import { CAMERA_PERMISSION_DENIED, TERMINATE_UPLOAD_ERROR } from "./constants"; const ALERT_TYPE = { camera: CAMERA_PERMISSION_DENIED, + terminateUpload: TERMINATE_UPLOAD_ERROR, }; export const showAlert = (alertType: string) => { const alert = ALERT_TYPE[alertType]; - const { title, message } = alert[Platform.OS]; + const { title, message } = alert[Platform.OS] ?? alert; Alert.alert(title, message); }; diff --git a/src/navigation/tab-navigator.tsx b/src/navigation/tab-navigator.tsx index 1800ce6..90ca9eb 100644 --- a/src/navigation/tab-navigator.tsx +++ b/src/navigation/tab-navigator.tsx @@ -4,10 +4,10 @@ import { Image } from "react-native"; import CameraScreen from "../component/video/camera"; import Profile from "../screens/profile"; import Session from "../screens/session"; -import RecordScreen from "../screens/video-stack/record"; +import VideoDetails from "../screens/video-stack/video-details"; import { tabIconColors } from "../styles"; -import Icon from "../assets/favicon.png"; +import Icon from "../assets/icons/favicon.png"; const Tab = createBottomTabNavigator(); const RecordStack = createNativeStackNavigator(); @@ -22,8 +22,17 @@ const tabIcons = { function VideoTabStack() { return ( - + + ); } diff --git a/src/screens/video-stack/record.tsx b/src/screens/video-stack/record.tsx deleted file mode 100644 index 9235498..0000000 --- a/src/screens/video-stack/record.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { - Keyboard, - Text, - TextInput, - TouchableOpacity, - TouchableWithoutFeedback, - View, -} from "react-native"; -import DropDownPicker from "react-native-dropdown-picker"; -// @ts-ignore -import { useCameraPermission } from "react-native-vision-camera"; -import { showAlert } from "../../lib/alert-messages"; -import { recordStyles as styles } from "./styles"; - -interface CameraScreenParams { - gameType: string; - tableSize: string; - tags: Array; - location: string; -} - -export default function RecordScreen({ navigation }): React.JSX.Element { - // Permissions - const { hasPermission, requestPermission } = useCameraPermission(); - - if (!hasPermission) { - requestPermission(); - } - - // Game type dropdown - const [gameTypeOpen, setGameTypeOpen] = useState(false); - const [gameType, setGameType] = useState(null); // This is a dropdown - const [gameTypes, setGameTypes] = useState([ - { label: "Free Play", value: "freePlay" }, - { label: "Straight Pool", value: "straightPool" }, - { label: "Nine Ball", value: "nineBall" }, - ]); - const onGameTypeOpen = useCallback(() => { - setTableSizeOpen(false); - setTagsOpen(false); - }, []); - - // Table size dropdown - const [tableSizeOpen, setTableSizeOpen] = useState(false); - const [tableSize, setTableSize] = useState(""); - const [tableSizes, setTableSizes] = useState([ - { label: `9'`, value: "nineFoot" }, - { label: `8'`, value: "eightFoot" }, - { label: "7", value: "sevenFoot" }, - ]); - const onTableSizeOpen = useCallback(() => { - setGameTypeOpen(false); - setTagsOpen(false); - }, []); - - // Tags multi-select dropdown - const [tagsOpen, setTagsOpen] = useState(false); - const [tags, setTags] = useState>([]); - const [tagsList, setTagsList] = useState([ - { label: `Tag1`, value: "tag1" }, - { label: `Tag2`, value: "tag2" }, - { label: "Tag3", value: "tag3" }, - ]); - const onTagsOpen = useCallback(() => { - setTableSizeOpen(false); - setGameTypeOpen(false); - }, []); - - // Location - const [location, setLocation] = useState(""); - - const handleSubmit = () => { - if (!hasPermission) { - return showAlert("camera"); - } - - // needs to pass info as params or store in a context/state provider - const params: CameraScreenParams = { - gameType: gameType, - tableSize: tableSize, - tags: tags, - location: location, - }; - navigation.push("Camera", params); - }; - - const dropDownStyles = { - style: styles.dropdownStyle, - }; - - return ( - Keyboard.dismiss()}> - - - Game Type - - Table size - - Tags - - - Location - setLocation(value)} - /> - - - navigation.goBack()} - > - Back - - - Next - - - - - ); -} diff --git a/src/screens/video-stack/styles.ts b/src/screens/video-stack/styles.ts index 458d271..21e1ce3 100644 --- a/src/screens/video-stack/styles.ts +++ b/src/screens/video-stack/styles.ts @@ -7,26 +7,13 @@ export const recordStyles = StyleSheet.create({ justifyContent: "center", padding: 20, }, - dropdownContainer: { - width: "100%", - marginBottom: 20, - zIndex: 50, - }, - dropdownTitle: { - fontSize: 16, + headerText: { + fontSize: 22, fontWeight: "500", - marginBottom: 5, - alignSelf: "flex-start", - }, - input: { - width: "100%", - marginBottom: 20, - borderWidth: 1, - borderColor: "grey", - borderRadius: 5, - padding: 10, + paddingBottom: "10%", }, buttonContainer: { + marginTop: 10, flexDirection: "row", justifyContent: "space-between", width: "100%", @@ -42,16 +29,4 @@ export const recordStyles = StyleSheet.create({ color: "white", textAlign: "center", }, - dropdownStyle: { - backgroundColor: "#ffffff", - borderColor: "#D1D1D1", - borderWidth: 1, - borderRadius: 4, - }, - dropdownContainerStyle: { - marginBottom: 10, - borderColor: "#D1D1D1", - borderWidth: 1, - borderRadius: 4, - }, }); diff --git a/src/screens/video-stack/video-details.tsx b/src/screens/video-stack/video-details.tsx new file mode 100644 index 0000000..ff871b9 --- /dev/null +++ b/src/screens/video-stack/video-details.tsx @@ -0,0 +1,179 @@ +import * as gql from "railbird-gql"; +import React, { useCallback, useState } from "react"; +import { + ActivityIndicator, + Keyboard, + Text, + TextInput, + TouchableOpacity, + TouchableWithoutFeedback, + View, +} from "react-native"; +import DropDownPicker from "react-native-dropdown-picker"; +// @ts-ignore +import { useCameraPermission } from "react-native-vision-camera"; +import { showAlert } from "../../lib/alert-messages"; +import { globalInputStyles } from "../../styles"; +import { recordStyles as styles } from "./styles"; +import { VideoFlowInputParams } from "./video-stack-types"; + +// TerminateUploadStream + +interface VideoDetailsProps { + navigation: any; + route?: any; +} + +export default function VideoDetails({ + navigation, + route, +}: VideoDetailsProps): React.JSX.Element { + const { hasPermission, requestPermission } = useCameraPermission(); + const { mode, videoId } = route.params; + if (mode === "start-video" && !hasPermission) { + requestPermission(); + } + + // Initial state values based on mode + const initialState = + mode === "save-video" && route.params ? route.params : {}; + + // Session name input + // Should session name be required? + const [sessionName, setSessionName] = useState( + initialState.sessionName || "", + ); + + // Game type dropdown + const [gameTypeOpen, setGameTypeOpen] = useState(false); + const [gameType, setGameType] = useState( + initialState.gameType || null, + ); + const [gameTypes, setGameTypes] = useState([ + { label: "Free Play", value: "freePlay" }, + { label: "Straight Pool", value: "straightPool" }, + { label: "Nine Ball", value: "nineBall" }, + ]); + const onGameTypeOpen = useCallback(() => { + setTableSizeOpen(false); + }, []); + + // Table size dropdown + const [tableSizeOpen, setTableSizeOpen] = useState(false); + const [tableSize, setTableSize] = useState( + initialState.tableSize || "", + ); + const [tableSizes, setTableSizes] = useState([ + { label: `9'`, value: "nineFoot" }, + { label: `8'`, value: "eightFoot" }, + { label: "7", value: "sevenFoot" }, + ]); + const onTableSizeOpen = useCallback(() => { + setGameTypeOpen(false); + }, []); + + const [TerminateUploadStream, { loading, error }] = + gql.useTerminateUploadStreamMutation(); + + const handleSubmit = async () => { + if (mode === "start-video" && !hasPermission) { + return showAlert("camera"); + } + + if (mode === "start-video") { + const params: VideoFlowInputParams = { + sessionName: sessionName, + gameType: gameType, + tableSize: tableSize, + }; + navigation.push("Camera", params); + } else { + try { + console.log(videoId, sessionName, tableSize, gameType); + const res = await TerminateUploadStream({ + variables: { + videoId: videoId, + videoName: sessionName, + tableSize: tableSize, + gameType: gameType, + }, + }); + console.log(res); + navigation.push("Tabs"); + } catch (err) { + console.error(error); + return showAlert("terminateUpload"); + } + } + }; + + const dropDownStyles = { + style: globalInputStyles.dropdownStyle, + }; + + return ( + Keyboard.dismiss()}> + + + {mode === "start-video" ? "Record Session" : "Save Session"} + + + Session Name + + Game Type + + Table size + + + + + {mode === "start-video" && ( + navigation.goBack()} + > + Back + + )} + + {loading ? ( + + ) : ( + + {mode === "start-video" ? "Next" : "Save"} + + )} + + + + + ); +} diff --git a/src/screens/video-stack/video-stack-types.ts b/src/screens/video-stack/video-stack-types.ts new file mode 100644 index 0000000..d796462 --- /dev/null +++ b/src/screens/video-stack/video-stack-types.ts @@ -0,0 +1,6 @@ +export interface VideoFlowInputParams { + sessionName?: string; + gameType: string; + tableSize: string; + videoId?: number; +} diff --git a/src/styles.ts b/src/styles.ts index 1cc4549..9b229b2 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1,4 +1,33 @@ // GLOBAL STYLES +import { StyleSheet } from "react-native"; + +export const globalInputStyles = StyleSheet.create({ + input: { + width: "100%", + padding: 10, + backgroundColor: "#ffffff", + borderColor: "#D1D1D1", + borderWidth: 1, + borderRadius: 4, + }, + dropdownContainer: { + width: "100%", + marginBottom: 20, + zIndex: 50, + }, + dropdownTitle: { + fontSize: 16, + fontWeight: "500", + marginBottom: 5, + alignSelf: "flex-start", + }, + dropdownStyle: { + backgroundColor: "#ffffff", + borderColor: "#D1D1D1", + borderWidth: 1, + borderRadius: 4, + }, +}); // COLORS: // can be made more granular to specify utility (ex: fontColors vs backgroundColors) export const colors = { diff --git a/yarn.lock b/yarn.lock index 74b6712..845aa3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9798,9 +9798,9 @@ queue@6.0.2: dependencies: inherits "~2.0.3" -"railbird-gql@git+https://dev.railbird.ai/railbird/railbird-gql.git#master": +"railbird-gql@git+https://dev.railbird.ai/railbird/railbird-gql.git#234d4d0fa90342f8af655c9ddf476f033caa322d": version "1.0.0" - resolved "git+https://dev.railbird.ai/railbird/railbird-gql.git#204e289627b08a96b947b6f23ed4a843d9e8abff" + resolved "git+https://dev.railbird.ai/railbird/railbird-gql.git#234d4d0fa90342f8af655c9ddf476f033caa322d" dependencies: "@apollo/client" "^3.9.2" "@graphql-codegen/cli" "^5.0.0"