expotesting2 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +289 -0
- package/apps/expo-app/app.json +60 -0
- package/apps/expo-app/babel.config.js +7 -0
- package/apps/expo-app/index.js +6 -0
- package/apps/expo-app/package.json +46 -0
- package/apps/expo-app/src/App.jsx +37 -0
- package/apps/expo-app/src/navigation/RootNavigator.jsx +82 -0
- package/apps/expo-app/src/navigation/types.js +5 -0
- package/apps/expo-app/src/screens/HomeScreen.jsx +178 -0
- package/package.json +24 -0
- package/packages/animations/package.json +20 -0
- package/packages/animations/src/components/FadeView.jsx +42 -0
- package/packages/animations/src/components/ScaleView.jsx +28 -0
- package/packages/animations/src/components/SlideView.jsx +32 -0
- package/packages/animations/src/hooks/useFade.js +50 -0
- package/packages/animations/src/hooks/useScale.js +59 -0
- package/packages/animations/src/hooks/useSlide.js +53 -0
- package/packages/animations/src/index.js +21 -0
- package/packages/animations/src/reanimated.js +83 -0
- package/packages/core/package.json +22 -0
- package/packages/core/src/components/Button.jsx +92 -0
- package/packages/core/src/components/Card.jsx +47 -0
- package/packages/core/src/components/Container.jsx +61 -0
- package/packages/core/src/components/Input.jsx +83 -0
- package/packages/core/src/components/List.jsx +80 -0
- package/packages/core/src/components/index.js +9 -0
- package/packages/core/src/hooks/index.js +5 -0
- package/packages/core/src/hooks/useAsync.js +60 -0
- package/packages/core/src/hooks/useCounter.js +36 -0
- package/packages/core/src/hooks/useToggle.js +18 -0
- package/packages/core/src/index.js +5 -0
- package/packages/core/src/theme/index.js +67 -0
- package/packages/core/src/utils/helpers.js +93 -0
- package/packages/core/src/utils/index.js +10 -0
- package/packages/device/package.json +24 -0
- package/packages/device/src/hooks/useCamera.js +45 -0
- package/packages/device/src/hooks/useGallery.js +70 -0
- package/packages/device/src/hooks/useLocation.js +99 -0
- package/packages/device/src/index.js +5 -0
- package/packages/examples/package.json +36 -0
- package/packages/examples/src/experiments/animations-device/AnimationsDeviceScreen.jsx +291 -0
- package/packages/examples/src/experiments/basic-app/BasicAppScreen.jsx +162 -0
- package/packages/examples/src/experiments/components-props-state/ComponentsStateScreen.jsx +280 -0
- package/packages/examples/src/experiments/navigation/NavigationScreen.jsx +202 -0
- package/packages/examples/src/experiments/network-storage/NetworkStorageScreen.jsx +367 -0
- package/packages/examples/src/experiments/state-management/StateManagementScreen.jsx +255 -0
- package/packages/examples/src/index.js +76 -0
- package/packages/navigation/package.json +20 -0
- package/packages/navigation/src/DrawerNavigator.jsx +35 -0
- package/packages/navigation/src/StackNavigator.jsx +51 -0
- package/packages/navigation/src/TabNavigator.jsx +44 -0
- package/packages/navigation/src/createAppNavigator.jsx +48 -0
- package/packages/navigation/src/index.js +8 -0
- package/packages/navigation/src/types.js +18 -0
- package/packages/network/package.json +19 -0
- package/packages/network/src/apiClient.js +90 -0
- package/packages/network/src/fetchHelpers.js +97 -0
- package/packages/network/src/hooks/useFetch.js +56 -0
- package/packages/network/src/index.js +3 -0
- package/packages/network/src/types.js +4 -0
- package/packages/state/package.json +22 -0
- package/packages/state/src/context/AuthContext.jsx +94 -0
- package/packages/state/src/context/ThemeContext.jsx +79 -0
- package/packages/state/src/context/index.js +3 -0
- package/packages/state/src/index.js +5 -0
- package/packages/state/src/redux/hooks.js +12 -0
- package/packages/state/src/redux/index.js +7 -0
- package/packages/state/src/redux/slices/counterSlice.js +39 -0
- package/packages/state/src/redux/slices/postsSlice.js +92 -0
- package/packages/state/src/redux/store.js +32 -0
- package/packages/storage/package.json +24 -0
- package/packages/storage/src/asyncStorage.js +82 -0
- package/packages/storage/src/index.js +2 -0
- package/packages/storage/src/sqlite/database.js +65 -0
- package/packages/storage/src/sqlite/index.js +3 -0
- package/packages/storage/src/sqlite/operations.js +112 -0
- package/packages/storage/src/sqlite/useSQLite.js +45 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCamera β manages Camera permission and provides a ref for expo-camera.
|
|
3
|
+
*
|
|
4
|
+
* The camera component itself is rendered by the consumer (useCamera only
|
|
5
|
+
* manages permissions and state), so this hook is fully decoupled from
|
|
6
|
+
* the camera UI.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* function CameraScreen() {
|
|
10
|
+
* const { permission, requestPermission, hasPermission } = useCamera();
|
|
11
|
+
*
|
|
12
|
+
* if (!hasPermission) {
|
|
13
|
+
* return (
|
|
14
|
+
* <View>
|
|
15
|
+
* <Text>Camera access needed</Text>
|
|
16
|
+
* <Button onPress={requestPermission}>Grant Permission</Button>
|
|
17
|
+
* </View>
|
|
18
|
+
* );
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* return <CameraView style={{ flex: 1 }} />;
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
26
|
+
import { Camera } from 'expo-camera';
|
|
27
|
+
|
|
28
|
+
export function useCamera(){
|
|
29
|
+
const [permission, setPermission] = useState(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
void Camera.getCameraPermissionsAsync().then((p) => setPermission(p.status));
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const requestPermission = useCallback(async () => {
|
|
36
|
+
const result = await Camera.requestCameraPermissionsAsync();
|
|
37
|
+
setPermission(result.status);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
permission,
|
|
42
|
+
hasPermission: permission === 'granted',
|
|
43
|
+
isDenied: permission === 'denied',
|
|
44
|
+
requestPermission };
|
|
45
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGallery β picks images/videos from the device gallery using expo-image-picker.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* function ImagePicker() {
|
|
6
|
+
* const { pickImage, asset, isLoading } = useGallery({ aspect: [4, 3] });
|
|
7
|
+
*
|
|
8
|
+
* return (
|
|
9
|
+
* <View>
|
|
10
|
+
* {asset && <Image source={{ uri: asset.uri }} style={{ width: 200, height: 150 }} />}
|
|
11
|
+
* <Button onPress={pickImage} loading={isLoading}>Pick from Gallery</Button>
|
|
12
|
+
* </View>
|
|
13
|
+
* );
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useCallback, useState } from 'react';
|
|
18
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
19
|
+
|
|
20
|
+
export function useGallery(options = {}){
|
|
21
|
+
const {
|
|
22
|
+
aspect,
|
|
23
|
+
allowsEditing = false,
|
|
24
|
+
quality = 0.8,
|
|
25
|
+
mediaTypes = ImagePicker.MediaTypeOptions.Images,
|
|
26
|
+
allowsMultipleSelection = false } = options;
|
|
27
|
+
|
|
28
|
+
const [asset, setAsset] = useState(null);
|
|
29
|
+
const [assets, setAssets] = useState([]);
|
|
30
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
|
|
33
|
+
const pickImage = useCallback(async () => {
|
|
34
|
+
setError(null);
|
|
35
|
+
setIsLoading(true);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Request permission if not already granted
|
|
39
|
+
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
40
|
+
if (status !== 'granted') {
|
|
41
|
+
setError('Gallery permission denied. Please allow access in Settings.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
46
|
+
mediaTypes,
|
|
47
|
+
allowsEditing,
|
|
48
|
+
aspect,
|
|
49
|
+
quality,
|
|
50
|
+
allowsMultipleSelection });
|
|
51
|
+
|
|
52
|
+
if (!result.canceled && result.assets.length > 0) {
|
|
53
|
+
setAssets(result.assets);
|
|
54
|
+
setAsset(result.assets[0] ?? null);
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setError(err instanceof Error ? err.message : 'Failed to pick image');
|
|
58
|
+
} finally {
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
}
|
|
61
|
+
}, [aspect, allowsEditing, quality, mediaTypes, allowsMultipleSelection]);
|
|
62
|
+
|
|
63
|
+
const clear = useCallback(() => {
|
|
64
|
+
setAsset(null);
|
|
65
|
+
setAssets([]);
|
|
66
|
+
setError(null);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
return { asset, assets, isLoading, error, pickImage, clear };
|
|
70
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLocation β fetches and optionally watches the device's GPS location.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* function MapScreen() {
|
|
6
|
+
* const { location, loading, error, getLocation } = useLocation({ watch: false });
|
|
7
|
+
*
|
|
8
|
+
* if (loading) return <ActivityIndicator />;
|
|
9
|
+
* if (error) return <Text>{error}</Text>;
|
|
10
|
+
*
|
|
11
|
+
* return (
|
|
12
|
+
* <Text>
|
|
13
|
+
* Lat: {location?.coords.latitude}, Lon: {location?.coords.longitude}
|
|
14
|
+
* </Text>
|
|
15
|
+
* );
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
20
|
+
import * as Location from 'expo-location';
|
|
21
|
+
|
|
22
|
+
export function useLocation(options = {}){
|
|
23
|
+
const {
|
|
24
|
+
watch = false,
|
|
25
|
+
distanceInterval = 10,
|
|
26
|
+
accuracy = Location.Accuracy.Balanced } = options;
|
|
27
|
+
|
|
28
|
+
const [location, setLocation] = useState(null);
|
|
29
|
+
const [address, setAddress] = useState(null);
|
|
30
|
+
const [loading, setLoading] = useState(false);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
const subscriptionRef = useRef(null);
|
|
33
|
+
|
|
34
|
+
const requestPermission = async ()=> {
|
|
35
|
+
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
36
|
+
if (status !== 'granted') {
|
|
37
|
+
setError('Location permission denied. Please allow access in Settings.');
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getLocation = useCallback(async () => {
|
|
44
|
+
setLoading(true);
|
|
45
|
+
setError(null);
|
|
46
|
+
try {
|
|
47
|
+
const granted = await requestPermission();
|
|
48
|
+
if (!granted) return;
|
|
49
|
+
|
|
50
|
+
const loc = await Location.getCurrentPositionAsync({ accuracy });
|
|
51
|
+
setLocation(loc);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
setError(err instanceof Error ? err.message : 'Failed to get location');
|
|
54
|
+
} finally {
|
|
55
|
+
setLoading(false);
|
|
56
|
+
}
|
|
57
|
+
}, [accuracy]);
|
|
58
|
+
|
|
59
|
+
const reverseGeocode = useCallback(async () => {
|
|
60
|
+
if (!location) {
|
|
61
|
+
setError('No location available to reverse geocode');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const results = await Location.reverseGeocodeAsync({
|
|
66
|
+
latitude: location.coords.latitude,
|
|
67
|
+
longitude: location.coords.longitude });
|
|
68
|
+
setAddress(results[0] ?? null);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
setError(err instanceof Error ? err.message : 'Reverse geocoding failed');
|
|
71
|
+
}
|
|
72
|
+
}, [location]);
|
|
73
|
+
|
|
74
|
+
// Set up watcher if watch=true
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!watch) return;
|
|
77
|
+
|
|
78
|
+
void (async () => {
|
|
79
|
+
const granted = await requestPermission();
|
|
80
|
+
if (!granted) return;
|
|
81
|
+
|
|
82
|
+
setLoading(true);
|
|
83
|
+
subscriptionRef.current = await Location.watchPositionAsync(
|
|
84
|
+
{ accuracy, distanceInterval },
|
|
85
|
+
(loc) => {
|
|
86
|
+
setLocation(loc);
|
|
87
|
+
setLoading(false);
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
subscriptionRef.current?.remove();
|
|
94
|
+
subscriptionRef.current = null;
|
|
95
|
+
};
|
|
96
|
+
}, [watch, accuracy, distanceInterval]);
|
|
97
|
+
|
|
98
|
+
return { location, address, loading, error, getLocation, reverseGeocode };
|
|
99
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@expotesting/examples",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Runnable experiment modules mapping to the university syllabus",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"scripts": {},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@expotesting/core": "workspace:*",
|
|
10
|
+
"@expotesting/navigation": "workspace:*",
|
|
11
|
+
"@expotesting/state": "workspace:*",
|
|
12
|
+
"@expotesting/network": "workspace:*",
|
|
13
|
+
"@expotesting/storage": "workspace:*",
|
|
14
|
+
"@expotesting/animations": "workspace:*",
|
|
15
|
+
"@expotesting/device": "workspace:*"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"react": ">=18",
|
|
19
|
+
"react-native": ">=0.73",
|
|
20
|
+
"expo": ">=51",
|
|
21
|
+
"@react-navigation/native": "^6",
|
|
22
|
+
"@react-navigation/native-stack": "^6",
|
|
23
|
+
"@react-navigation/bottom-tabs": "^6",
|
|
24
|
+
"@react-navigation/drawer": "^6",
|
|
25
|
+
"@reduxjs/toolkit": "^2",
|
|
26
|
+
"react-redux": "^9",
|
|
27
|
+
"axios": "^1",
|
|
28
|
+
"@react-native-async-storage/async-storage": ">=1",
|
|
29
|
+
"expo-sqlite": ">=14",
|
|
30
|
+
"react-native-reanimated": ">=3",
|
|
31
|
+
"expo-camera": ">=15",
|
|
32
|
+
"expo-image-picker": ">=15",
|
|
33
|
+
"expo-location": ">=17"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiment 06 β Animations & Device Features
|
|
3
|
+
* Module 3: Animated API Β· Reanimated Β· Camera Β· Gallery Β· GPS
|
|
4
|
+
*
|
|
5
|
+
* Demonstrates:
|
|
6
|
+
* β Fade animation (Animated API)
|
|
7
|
+
* β Scale animation (spring)
|
|
8
|
+
* β Slide animation
|
|
9
|
+
* β Reanimated entering/exiting animations
|
|
10
|
+
* β Camera capture (Expo Camera)
|
|
11
|
+
* β Image gallery picker (Expo ImagePicker)
|
|
12
|
+
* β GPS location (Expo Location)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import React, { useState } from 'react';
|
|
16
|
+
import {
|
|
17
|
+
Image,
|
|
18
|
+
Pressable,
|
|
19
|
+
ScrollView,
|
|
20
|
+
StyleSheet,
|
|
21
|
+
Text,
|
|
22
|
+
View } from 'react-native';
|
|
23
|
+
import Animated, { FadeIn, FadeOut, ZoomIn, SlideInLeft } from 'react-native-reanimated';
|
|
24
|
+
|
|
25
|
+
import { FadeView, ScaleView, SlideView } from '@expotesting/animations';
|
|
26
|
+
import { useCamera, useGallery, useLocation } from '@expotesting/device';
|
|
27
|
+
|
|
28
|
+
// ββ Animations Section ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
29
|
+
function AnimationsSection(){
|
|
30
|
+
const [showFade, setShowFade] = useState(true);
|
|
31
|
+
const [showScale, setShowScale] = useState(true);
|
|
32
|
+
const [showSlide, setShowSlide] = useState(true);
|
|
33
|
+
const [showReanimated, setShowReanimated] = useState(false);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<View style={styles.section}>
|
|
37
|
+
<Text style={styles.sectionTitle}>Animations</Text>
|
|
38
|
+
|
|
39
|
+
{/* Fade */}
|
|
40
|
+
<View style={styles.widget}>
|
|
41
|
+
<Text style={styles.widgetTitle}>FadeView β Animated API</Text>
|
|
42
|
+
<FadeView visible={showFade} duration={400}>
|
|
43
|
+
<View style={[styles.animBox, { backgroundColor: '#E1BEE7' }]}>
|
|
44
|
+
<Text style={styles.animLabel}>Fading content</Text>
|
|
45
|
+
</View>
|
|
46
|
+
</FadeView>
|
|
47
|
+
<Pressable style={styles.btn} onPress={() => setShowFade((v) => !v)}>
|
|
48
|
+
<Text style={styles.btnText}>{showFade ? 'Fade Out' : 'Fade In'}</Text>
|
|
49
|
+
</Pressable>
|
|
50
|
+
</View>
|
|
51
|
+
|
|
52
|
+
{/* Scale */}
|
|
53
|
+
<View style={styles.widget}>
|
|
54
|
+
<Text style={styles.widgetTitle}>ScaleView β Spring animation</Text>
|
|
55
|
+
<ScaleView visible={showScale}>
|
|
56
|
+
<View style={[styles.animBox, { backgroundColor: '#B2EBF2' }]}>
|
|
57
|
+
<Text style={styles.animLabel}>Scaling content</Text>
|
|
58
|
+
</View>
|
|
59
|
+
</ScaleView>
|
|
60
|
+
<Pressable style={styles.btn} onPress={() => setShowScale((v) => !v)}>
|
|
61
|
+
<Text style={styles.btnText}>{showScale ? 'Scale Out' : 'Scale In'}</Text>
|
|
62
|
+
</Pressable>
|
|
63
|
+
</View>
|
|
64
|
+
|
|
65
|
+
{/* Slide */}
|
|
66
|
+
<View style={styles.widget}>
|
|
67
|
+
<Text style={styles.widgetTitle}>SlideView β Slide from top</Text>
|
|
68
|
+
<SlideView visible={showSlide} direction="top">
|
|
69
|
+
<View style={[styles.animBox, { backgroundColor: '#C8E6C9' }]}>
|
|
70
|
+
<Text style={styles.animLabel}>Sliding content</Text>
|
|
71
|
+
</View>
|
|
72
|
+
</SlideView>
|
|
73
|
+
<Pressable style={styles.btn} onPress={() => setShowSlide((v) => !v)}>
|
|
74
|
+
<Text style={styles.btnText}>{showSlide ? 'Slide Out' : 'Slide In'}</Text>
|
|
75
|
+
</Pressable>
|
|
76
|
+
</View>
|
|
77
|
+
|
|
78
|
+
{/* Reanimated entering / exiting */}
|
|
79
|
+
<View style={styles.widget}>
|
|
80
|
+
<Text style={styles.widgetTitle}>Reanimated β Entering / Exiting</Text>
|
|
81
|
+
<Text style={styles.noteSmall}>
|
|
82
|
+
Uses entering={'{'}FadeIn{'}'} / exiting={'{'}FadeOut{'}'} / ZoomIn / SlideInLeft
|
|
83
|
+
</Text>
|
|
84
|
+
<Pressable style={styles.btn} onPress={() => setShowReanimated((v) => !v)}>
|
|
85
|
+
<Text style={styles.btnText}>{showReanimated ? 'Hide' : 'Show'} Animated Views</Text>
|
|
86
|
+
</Pressable>
|
|
87
|
+
{showReanimated && (
|
|
88
|
+
<View style={styles.row}>
|
|
89
|
+
<Animated.View entering={FadeIn.duration(500)} exiting={FadeOut.duration(300)} style={[styles.reaBox, { backgroundColor: '#F8BBD9' }]}>
|
|
90
|
+
<Text style={styles.reaLabel}>FadeIn</Text>
|
|
91
|
+
</Animated.View>
|
|
92
|
+
<Animated.View entering={ZoomIn.springify()} exiting={FadeOut} style={[styles.reaBox, { backgroundColor: '#DCEDC8' }]}>
|
|
93
|
+
<Text style={styles.reaLabel}>ZoomIn</Text>
|
|
94
|
+
</Animated.View>
|
|
95
|
+
<Animated.View entering={SlideInLeft.duration(400)} exiting={FadeOut} style={[styles.reaBox, { backgroundColor: '#B3E5FC' }]}>
|
|
96
|
+
<Text style={styles.reaLabel}>SlideLeft</Text>
|
|
97
|
+
</Animated.View>
|
|
98
|
+
</View>
|
|
99
|
+
)}
|
|
100
|
+
</View>
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ββ Camera Section ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
106
|
+
function CameraSection(){
|
|
107
|
+
const { hasPermission, requestPermission, capturePhoto, lastPhoto, isCapturing } = useCamera();
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<View style={styles.widget}>
|
|
111
|
+
<Text style={styles.widgetTitle}>Camera (Expo Camera)</Text>
|
|
112
|
+
{!hasPermission ? (
|
|
113
|
+
<Pressable style={styles.btn} onPress={() => void requestPermission()}>
|
|
114
|
+
<Text style={styles.btnText}>Grant Camera Permission</Text>
|
|
115
|
+
</Pressable>
|
|
116
|
+
) : (
|
|
117
|
+
<Pressable
|
|
118
|
+
style={[styles.btn, isCapturing && styles.btnDisabled]}
|
|
119
|
+
onPress={() => void capturePhoto()}
|
|
120
|
+
disabled={isCapturing}
|
|
121
|
+
>
|
|
122
|
+
<Text style={styles.btnText}>{isCapturing ? 'Capturingβ¦' : 'π· Capture Photo'}</Text>
|
|
123
|
+
</Pressable>
|
|
124
|
+
)}
|
|
125
|
+
{lastPhoto && (
|
|
126
|
+
<Image
|
|
127
|
+
source={{ uri: lastPhoto.uri }}
|
|
128
|
+
style={styles.previewImage}
|
|
129
|
+
accessibilityLabel="Captured photo preview"
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
</View>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ββ Gallery Section βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
137
|
+
function GallerySection(){
|
|
138
|
+
const { hasPermission, requestPermission, pickImage, selectedAsset, isPicking } = useGallery({
|
|
139
|
+
allowsEditing: true,
|
|
140
|
+
aspect: [4, 3] });
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<View style={styles.widget}>
|
|
144
|
+
<Text style={styles.widgetTitle}>Gallery (Expo ImagePicker)</Text>
|
|
145
|
+
{!hasPermission ? (
|
|
146
|
+
<Pressable style={styles.btn} onPress={() => void requestPermission()}>
|
|
147
|
+
<Text style={styles.btnText}>Grant Gallery Permission</Text>
|
|
148
|
+
</Pressable>
|
|
149
|
+
) : (
|
|
150
|
+
<Pressable
|
|
151
|
+
style={[styles.btn, isPicking && styles.btnDisabled]}
|
|
152
|
+
onPress={() => void pickImage()}
|
|
153
|
+
disabled={isPicking}
|
|
154
|
+
>
|
|
155
|
+
<Text style={styles.btnText}>{isPicking ? 'Pickingβ¦' : 'πΌ Pick from Gallery'}</Text>
|
|
156
|
+
</Pressable>
|
|
157
|
+
)}
|
|
158
|
+
{selectedAsset && (
|
|
159
|
+
<Image
|
|
160
|
+
source={{ uri: selectedAsset.uri }}
|
|
161
|
+
style={styles.previewImage}
|
|
162
|
+
accessibilityLabel="Selected image preview"
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
</View>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ββ Location Section ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
170
|
+
function LocationSection(){
|
|
171
|
+
const { location, hasPermission, requestPermission, getLocation, loading, error } = useLocation({
|
|
172
|
+
accuracy: 'balanced' });
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<View style={styles.widget}>
|
|
176
|
+
<Text style={styles.widgetTitle}>GPS Location (Expo Location)</Text>
|
|
177
|
+
{!hasPermission ? (
|
|
178
|
+
<Pressable style={styles.btn} onPress={() => void requestPermission()}>
|
|
179
|
+
<Text style={styles.btnText}>Grant Location Permission</Text>
|
|
180
|
+
</Pressable>
|
|
181
|
+
) : (
|
|
182
|
+
<Pressable
|
|
183
|
+
style={[styles.btn, loading && styles.btnDisabled]}
|
|
184
|
+
onPress={() => void getLocation()}
|
|
185
|
+
disabled={loading}
|
|
186
|
+
>
|
|
187
|
+
<Text style={styles.btnText}>{loading ? 'Getting locationβ¦' : 'π Get My Location'}</Text>
|
|
188
|
+
</Pressable>
|
|
189
|
+
)}
|
|
190
|
+
{error && <Text style={styles.errorText}>{error}</Text>}
|
|
191
|
+
{location && (
|
|
192
|
+
<View style={styles.locationBox}>
|
|
193
|
+
<LocationRow label="Latitude" value={location.coords.latitude.toFixed(6)} />
|
|
194
|
+
<LocationRow label="Longitude" value={location.coords.longitude.toFixed(6)} />
|
|
195
|
+
<LocationRow label="Accuracy" value={`${(location.coords.accuracy ?? 0).toFixed(0)}m`} />
|
|
196
|
+
<LocationRow label="Altitude" value={`${(location.coords.altitude ?? 0).toFixed(1)}m`} />
|
|
197
|
+
</View>
|
|
198
|
+
)}
|
|
199
|
+
</View>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function LocationRow({ label, value }){
|
|
204
|
+
return (
|
|
205
|
+
<View style={styles.locationRow}>
|
|
206
|
+
<Text style={styles.locationLabel}>{label}</Text>
|
|
207
|
+
<Text style={styles.locationValue}>{value}</Text>
|
|
208
|
+
</View>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ββ Main screen βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
213
|
+
export function AnimationsDeviceScreen(){
|
|
214
|
+
return (
|
|
215
|
+
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
216
|
+
<View style={styles.header}>
|
|
217
|
+
<Text style={styles.heading}>Experiment 06</Text>
|
|
218
|
+
<Text style={styles.subheading}>Animations & Device Features</Text>
|
|
219
|
+
</View>
|
|
220
|
+
|
|
221
|
+
<AnimationsSection />
|
|
222
|
+
|
|
223
|
+
<View style={styles.section}>
|
|
224
|
+
<Text style={styles.sectionTitle}>Device Features</Text>
|
|
225
|
+
<CameraSection />
|
|
226
|
+
<GallerySection />
|
|
227
|
+
<LocationSection />
|
|
228
|
+
</View>
|
|
229
|
+
</ScrollView>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ββ Styles ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
234
|
+
const styles = StyleSheet.create({
|
|
235
|
+
container: { flex: 1, backgroundColor: '#fff' },
|
|
236
|
+
content: { paddingBottom: 40 },
|
|
237
|
+
header: { backgroundColor: '#6200EE', padding: 24, paddingTop: 40 },
|
|
238
|
+
heading: { fontSize: 28, fontWeight: '700', color: '#fff' },
|
|
239
|
+
subheading: { fontSize: 16, color: 'rgba(255,255,255,0.8)', marginTop: 4 },
|
|
240
|
+
section: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#e0e0e0' },
|
|
241
|
+
sectionTitle: { fontSize: 18, fontWeight: '700', color: '#333', marginBottom: 12 },
|
|
242
|
+
widget: { backgroundColor: '#f9f9f9', borderRadius: 12, padding: 14, marginBottom: 12 },
|
|
243
|
+
widgetTitle: { fontSize: 15, fontWeight: '700', color: '#333', marginBottom: 8 },
|
|
244
|
+
noteSmall: { fontSize: 12, color: '#888', marginBottom: 8 },
|
|
245
|
+
btn: {
|
|
246
|
+
backgroundColor: '#6200EE',
|
|
247
|
+
paddingVertical: 10,
|
|
248
|
+
paddingHorizontal: 20,
|
|
249
|
+
borderRadius: 8,
|
|
250
|
+
alignItems: 'center',
|
|
251
|
+
justifyContent: 'center' },
|
|
252
|
+
btnDisabled: { opacity: 0.5 },
|
|
253
|
+
btnText: { color: '#fff', fontWeight: '700', fontSize: 14 },
|
|
254
|
+
errorText: { color: '#B00020', fontSize: 13, marginVertical: 4 },
|
|
255
|
+
animBox: {
|
|
256
|
+
height: 64,
|
|
257
|
+
borderRadius: 8,
|
|
258
|
+
alignItems: 'center',
|
|
259
|
+
justifyContent: 'center',
|
|
260
|
+
marginBottom: 8 },
|
|
261
|
+
animLabel: { fontSize: 14, fontWeight: '600', color: '#333' },
|
|
262
|
+
row: { flexDirection: 'row', gap: 8, marginTop: 8, flexWrap: 'wrap' },
|
|
263
|
+
reaBox: {
|
|
264
|
+
flex: 1,
|
|
265
|
+
height: 60,
|
|
266
|
+
borderRadius: 8,
|
|
267
|
+
alignItems: 'center',
|
|
268
|
+
justifyContent: 'center',
|
|
269
|
+
minWidth: 80 },
|
|
270
|
+
reaLabel: { fontSize: 11, fontWeight: '700', color: '#333' },
|
|
271
|
+
previewImage: {
|
|
272
|
+
width: '100%',
|
|
273
|
+
height: 200,
|
|
274
|
+
borderRadius: 8,
|
|
275
|
+
marginTop: 10,
|
|
276
|
+
backgroundColor: '#eee' },
|
|
277
|
+
locationBox: {
|
|
278
|
+
backgroundColor: '#fff',
|
|
279
|
+
borderRadius: 8,
|
|
280
|
+
padding: 10,
|
|
281
|
+
marginTop: 8,
|
|
282
|
+
borderWidth: 1,
|
|
283
|
+
borderColor: '#e0e0e0' },
|
|
284
|
+
locationRow: {
|
|
285
|
+
flexDirection: 'row',
|
|
286
|
+
justifyContent: 'space-between',
|
|
287
|
+
paddingVertical: 4,
|
|
288
|
+
borderBottomWidth: 1,
|
|
289
|
+
borderBottomColor: '#f0f0f0' },
|
|
290
|
+
locationLabel: { fontSize: 13, color: '#666' },
|
|
291
|
+
locationValue: { fontSize: 13, fontWeight: '700', color: '#333' } });
|