diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 77de233..7fc75dc 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -3,12 +3,19 @@ + + + + + + NSMicrophoneUsageDescription VisionCamera needs access to your Microphone to record videos with audio. + NSPhotoLibraryUsageDescription + VisionCamera needs access to your photo library to save captured videos and photos. UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/example/package-lock.json b/example/package-lock.json index d73b5d2..2e1c8f2 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -1015,6 +1015,11 @@ "prop-types": "^15.5.10" } }, + "@react-native-community/cameraroll": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-native-community/cameraroll/-/cameraroll-4.0.2.tgz", + "integrity": "sha512-GtSZO6pqUzyZvaYidB5zH90o6Yb9YatapgiMQ+JVdbK4bDD74GdrNGDwyinDTzE5LkAQ90HDoAhVgV/uWt5OrQ==" + }, "@react-native-community/cli-debugger-ui": { "version": "4.13.1", "resolved": "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-4.13.1.tgz", diff --git a/example/package.json b/example/package.json index d89b465..30b780c 100644 --- a/example/package.json +++ b/example/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@react-native-community/blur": "^3.6.0", + "@react-native-community/cameraroll": "^4.0.2", "react": "16.13.1", "react-native": "0.63.4", "react-native-gesture-handler": "^1.10.1", diff --git a/example/src/Media.tsx b/example/src/Media.tsx index a24743b..468907b 100644 --- a/example/src/Media.tsx +++ b/example/src/Media.tsx @@ -1,34 +1,85 @@ -import React, { useCallback, useMemo } from 'react'; -import { StyleSheet, View, Text, Image, Pressable } from 'react-native'; -import { Navigation, NavigationFunctionComponent } from 'react-native-navigation'; +import React, { useCallback, useMemo, useState } from 'react'; +import { StyleSheet, View, Image, ActivityIndicator, PermissionsAndroid, Platform } from 'react-native'; +import { Navigation, NavigationFunctionComponent, OptionsModalPresentationStyle } from 'react-native-navigation'; import Video from 'react-native-video'; -import { CONTENT_SPACING } from './Constants'; +import { SAFE_AREA_PADDING } from './Constants'; import { useIsForeground } from './hooks/useIsForeground'; import { useIsScreenFocused } from './hooks/useIsScreenFocused'; +import { PressableOpacity } from './views/PressableOpacity'; +import IonIcon from 'react-native-vector-icons/Ionicons'; +import { Alert } from 'react-native'; +import CameraRoll from '@react-native-community/cameraroll'; interface MediaProps { path: string, type: 'video' | 'photo' } +const requestSavePermission = async (): Promise => { + 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"; + } + return hasPermission; +} + export const Media: NavigationFunctionComponent = ({ 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 onClosePressed = useCallback(() => { Navigation.dismissModal(componentId); }, [componentId]); - const source = useMemo(() => ({ uri: `file://${path}` }), [path]) + const onMediaLoadEnd = useCallback(() => { + console.log(`media has loaded.`); + setHasMediaLoaded(true); + }, []); + + const onSavePressed = useCallback(async () => { + try { + setSavingState("saving"); + + const hasPermission = await requestSavePermission(); + if (!hasPermission) { + Alert.alert('Permission denied!', 'Vision Camera does not have permission to save the media to your camera roll.'); + return; + } + await CameraRoll.save(`file://${path}`, { + type: type, + }); + 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)}` + ); + } + }, [path, type]); + + const source = useMemo(() => ({ uri: `file://${path}` }), [path]); + + const screenStyle = useMemo(() => ({ opacity: hasMediaLoaded ? 1 : 0 }), [ + hasMediaLoaded, + ]); return ( - + {type === "photo" && ( + resizeMode="cover" + onLoadEnd={onMediaLoadEnd} /> )} {type === "video" && ( ); } +Media.options = { + modal: { + swipeToDismiss: false, + }, + modalPresentationStyle: OptionsModalPresentationStyle.overCurrentContext, + animations: { + showModal: { + waitForRender: true, + enabled: false, + }, + dismissModal: { + enabled: false, + }, + }, + layout: { + backgroundColor: "transparent", + componentBackgroundColor: "transparent", + }, +}; + const styles = StyleSheet.create({ container: { flex: 1, @@ -60,9 +165,24 @@ const styles = StyleSheet.create({ }, closeButton: { position: 'absolute', - top: CONTENT_SPACING, - left: CONTENT_SPACING, + top: SAFE_AREA_PADDING.paddingTop, + left: SAFE_AREA_PADDING.paddingLeft, width: 40, height: 40, }, + saveButton: { + position: 'absolute', + bottom: SAFE_AREA_PADDING.paddingBottom, + left: SAFE_AREA_PADDING.paddingLeft, + width: 40, + height: 40, + }, + icon: { + textShadowColor: "black", + textShadowOffset: { + height: 0, + width: 0, + }, + textShadowRadius: 1, + }, });