Commit 7b119d30 by tungnq

feat: Nâng cấp màn hình hồ sơ với chọn ảnh & chụp ảnh

Tích hợp image picker cho phép chọn ảnh từ thư viện.

Thêm chức năng camera để chụp ảnh trực tiếp trong ứng dụng.

Tải nhanh ảnh gần đây từ Camera Roll để chọn tức thời.

Cập nhật style cho cụm điều khiển camera và khung xem trước ảnh.

Cải thiện trải nghiệm: quản lý hiển thị tab (ẩn/hiện) khi mở camera và khi xem trước ảnh.
parent a658561e
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
android:label="@string/app_name" android:label="@string/app_name"
......
...@@ -9,6 +9,9 @@ const images = { ...@@ -9,6 +9,9 @@ const images = {
icGallery: require('./images/icGallery.png'), icGallery: require('./images/icGallery.png'),
icNoData: require('./icon/icon_png/icon_no_data.png'), icNoData: require('./icon/icon_png/icon_no_data.png'),
icMenuEdit: require('./icon/icon_png/menuEdit.png'), icMenuEdit: require('./icon/icon_png/menuEdit.png'),
icSwitchCamera: require('./icon/icon_png/icon_switch_camera.png'),
icClose: require('./icon/icon_png/icon_close.png'),
icTakePhoto: require('./icon/icon_png/icon_take_photo.png'),
//HomeScreen //HomeScreen
icLichDay:require('./icon/lich_day.png'), icLichDay:require('./icon/lich_day.png'),
icBaoBu: require('./icon/bao_bu.png'), icBaoBu: require('./icon/bao_bu.png'),
......
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import {DeviceEventEmitter, Image, View} from 'react-native'; import {DeviceEventEmitter, Image, View} from 'react-native';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {useFocusEffect} from '@react-navigation/native';
import i18n from '../helper/i18/i18n'; import i18n from '../helper/i18/i18n';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import R from '../assets/R'; import R from '../assets/R';
...@@ -15,26 +16,31 @@ const Tab = createBottomTabNavigator(); ...@@ -15,26 +16,31 @@ const Tab = createBottomTabNavigator();
const TabNavigator = props => { const TabNavigator = props => {
const [reload, setReload] = useState(false); const [reload, setReload] = useState(false);
const [hideTabBar, setHideTabBar] = useState(false);
useEffect(() => { useEffect(() => {
let setLanguage = DeviceEventEmitter.addListener('setLanguage', value => { let setLanguage = DeviceEventEmitter.addListener('setLanguage', value => {
setReload(!reload); setReload(!reload);
}); });
let hideTabs = DeviceEventEmitter.addListener('hideTabs', shouldHide => {
setHideTabBar(shouldHide);
});
return () => { return () => {
setLanguage.remove(); setLanguage.remove();
hideTabs.remove();
}; };
}, []); }, []);
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName="Screen5" initialRouteName="Screen5"
screenOptions={{headerShown: false}} screenOptions={{
tabBarOptions={{ headerShown: false,
showIcon: true, tabBarStyle: hideTabBar
showLabel: true, ? {display: 'none'}
activeTintColor: R.colors.main, : {
style: {
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { shadowOffset: {
width: 0, width: 0,
...@@ -45,6 +51,11 @@ const TabNavigator = props => { ...@@ -45,6 +51,11 @@ const TabNavigator = props => {
elevation: 7, elevation: 7,
justifyContent: 'center', justifyContent: 'center',
}, },
}}
tabBarOptions={{
showIcon: true,
showLabel: true,
activeTintColor: R.colors.main,
}}> }}>
<Tab.Screen <Tab.Screen
name="HomeScreen1" name="HomeScreen1"
......
import {StyleSheet, Text, View} from 'react-native'; import {StyleSheet, Text, View, Dimensions} from 'react-native';
import R from '../../assets/R'; import R from '../../assets/R';
const {width, height} = Dimensions.get('window');
const widthLibary = width / 7;
const heightLibary = height / 14;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
...@@ -59,13 +62,212 @@ const styles = StyleSheet.create({ ...@@ -59,13 +62,212 @@ const styles = StyleSheet.create({
sizedBox: { sizedBox: {
width: 15, width: 15,
}, },
topLeft: {
position: 'absolute',
top: 15,
left: 15,
},
topRight: {
position: 'absolute',
top: 15,
right: 15,
},
bottomLeft: {
position: 'absolute',
bottom: heightLibary,
left: widthLibary,
},
bottomRight: {
position: 'absolute',
bottom: 15,
right: 15,
},
shutter: {
position: 'absolute',
bottom: 50,
left: '40%',
width: 68,
height: 68,
borderRadius: 34,
borderWidth: 4,
borderColor: '#fff',
backgroundColor: 'rgba(255,255,255,0.2)',
},
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'black',
},
smallBtn: {
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
backgroundColor: 'transparent',
},
btnText: {color: '#fff', fontWeight: '600'},
absoluteFill: {position: 'absolute', top: 0, left: 0, right: 0, bottom: 0},
center: {justifyContent: 'center', alignItems: 'center'},
// Gallery Preview Styles
galleryPreview: {
width: 60,
height: 60,
borderRadius: 8,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: 'rgba(255, 255, 255, 0.3)',
},
galleryGrid: {
width: '100%',
height: '100%',
flexDirection: 'row',
flexWrap: 'wrap',
borderRadius: 6,
overflow: 'hidden',
},
galleryThumbnail: {
width: '50%',
height: '50%',
borderWidth: 0.5,
borderColor: 'rgba(255, 255, 255, 0.2)',
},
// Image Source Modal Styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContainer: {
backgroundColor: R.colors.white,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingHorizontal: 20,
paddingVertical: 30,
minHeight: 250,
},
modalTitle: {
fontSize: R.sizes.lg,
fontFamily: R.fonts.fontMedium,
fontWeight: '600',
color: R.colors.black,
textAlign: 'center',
marginBottom: 30,
},
modalOption: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 15,
paddingHorizontal: 20,
backgroundColor: R.colors.grayLight || '#f5f5f5',
borderRadius: 10,
marginBottom: 15,
},
modalIcon: {
width: 24,
height: 24,
marginRight: 15,
tintColor: R.colors.blue,
},
modalOptionText: {
fontSize: R.sizes.md,
fontFamily: R.fonts.fontMedium,
fontWeight: '500',
color: R.colors.black,
},
modalCancelButton: {
paddingVertical: 15,
alignItems: 'center',
marginTop: 10,
},
modalCancelText: {
fontSize: R.sizes.md,
fontFamily: R.fonts.fontMedium,
fontWeight: '500',
color: R.colors.gray,
},
overlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'black' }, // Preview Modal Styles
controls: { position: 'absolute', bottom: 30, left: 0, right: 0, flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center' }, previewContainer: {
shutter: { width: 68, height: 68, borderRadius: 34, borderWidth: 4, borderColor: '#fff', backgroundColor: 'rgba(255,255,255,0.2)' }, flex: 1,
smallBtn: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8, backgroundColor: 'rgba(255,255,255,0.2)' }, backgroundColor: R.colors.black,
btnText: { color: '#fff', fontWeight: '600' }, },
absoluteFill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }, previewHeader: {
center: { justifyContent: 'center', alignItems: 'center' }, flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 15,
paddingTop: 50,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
},
previewCloseButton: {
padding: 10,
},
previewCloseIcon: {
width: 24,
height: 24,
tintColor: R.colors.white,
},
previewTitle: {
fontSize: R.sizes.lg,
fontFamily: R.fonts.fontMedium,
fontWeight: '600',
color: R.colors.white,
},
previewSpacer: {
width: 44,
},
previewImageContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
previewImage: {
width: '100%',
height: '100%',
},
previewActions: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingHorizontal: 40,
paddingVertical: 30,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
},
previewRetakeButton: {
paddingVertical: 12,
paddingHorizontal: 30,
borderRadius: 25,
borderWidth: 2,
borderColor: R.colors.white,
backgroundColor: 'transparent',
},
previewRetakeText: {
fontSize: R.sizes.md,
fontFamily: R.fonts.fontMedium,
fontWeight: '600',
color: R.colors.white,
textAlign: 'center',
},
previewConfirmButton: {
paddingVertical: 12,
paddingHorizontal: 30,
borderRadius: 25,
backgroundColor: R.colors.blue,
},
previewConfirmText: {
fontSize: R.sizes.md,
fontFamily: R.fonts.fontMedium,
fontWeight: '600',
color: R.colors.white,
textAlign: 'center',
},
}); });
export default styles; export default styles;
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
Image, Image,
ScrollView, ScrollView,
TouchableOpacity, TouchableOpacity,
Modal,
} from 'react-native'; } from 'react-native';
import styles from './style'; import styles from './style';
import Header from '../../components/Header/Header'; import Header from '../../components/Header/Header';
...@@ -12,9 +13,7 @@ import R from '../../assets/R'; ...@@ -12,9 +13,7 @@ import R from '../../assets/R';
import Button from '../../components/Button'; import Button from '../../components/Button';
import TextField from '../../components/Input/TextField'; import TextField from '../../components/Input/TextField';
import RadioGroup from '../../components/RadioButton/RadioGroup'; import RadioGroup from '../../components/RadioButton/RadioGroup';
import { import {Camera} from 'react-native-vision-camera';
Camera,
} from 'react-native-vision-camera'
const ProfileView = props => { const ProfileView = props => {
const { const {
dataProfile, dataProfile,
...@@ -29,6 +28,21 @@ const ProfileView = props => { ...@@ -29,6 +28,21 @@ const ProfileView = props => {
onTakePhoto, onTakePhoto,
onCloseCamera, onCloseCamera,
// image source modal
showImageSourceModal,
onCloseImageSourceModal,
onSelectCamera,
onSelectGallery,
// preview
showPreview,
previewUri,
onConfirmPhoto,
onRetakePhoto,
onClosePreview,
// gallery preview
recentPhotos,
selectedValue2, selectedValue2,
options2, options2,
onValueChange2, onValueChange2,
...@@ -101,9 +115,6 @@ const ProfileView = props => { ...@@ -101,9 +115,6 @@ const ProfileView = props => {
onSave, onSave,
} = props; } = props;
const renderButtonCamera = () => { const renderButtonCamera = () => {
return ( return (
<Button <Button
...@@ -122,6 +133,55 @@ const ProfileView = props => { ...@@ -122,6 +133,55 @@ const ProfileView = props => {
); );
}; };
const renderPreviewModal = () => {
return (
<Modal
visible={showPreview}
transparent={false}
animationType="slide"
onRequestClose={onClosePreview}>
<View style={styles.previewContainer}>
<View style={styles.previewHeader}>
<TouchableOpacity
style={styles.previewCloseButton}
onPress={onClosePreview}>
<Image
source={R.images.icClose}
style={styles.previewCloseIcon}
/>
</TouchableOpacity>
<Text style={styles.previewTitle}>Xem trước nh</Text>
<View style={styles.previewSpacer} />
</View>
<View style={styles.previewImageContainer}>
{previewUri && (
<Image
source={{uri: previewUri}}
style={styles.previewImage}
resizeMode="contain"
/>
)}
</View>
<View style={styles.previewActions}>
<TouchableOpacity
style={styles.previewRetakeButton}
onPress={onRetakePhoto}>
<Text style={styles.previewRetakeText}>Chp li</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.previewConfirmButton}
onPress={onConfirmPhoto}>
<Text style={styles.previewConfirmText}>Xác nhn</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
};
const renderHeaderBody = () => { const renderHeaderBody = () => {
return ( return (
<View style={styles.headerBody}> <View style={styles.headerBody}>
...@@ -129,9 +189,12 @@ const ProfileView = props => { ...@@ -129,9 +189,12 @@ const ProfileView = props => {
<View style={styles.boxCamera}> <View style={styles.boxCamera}>
<View style={styles.containerImage}> <View style={styles.containerImage}>
{avatarUri ? ( {avatarUri ? (
<Image source={{ uri: avatarUri }} style={styles.image} /> <Image source={{uri: avatarUri}} style={styles.image} />
) : ( ) : (
<Image source={R.images.iconCamera} style={styles.image} /> <Image
source={R.images.iconCamera}
style={[styles.image, {width: 25, height: 25}]}
/>
)} )}
</View> </View>
<View style={styles.containerButton}>{renderButtonCamera()}</View> <View style={styles.containerButton}>{renderButtonCamera()}</View>
...@@ -690,6 +753,10 @@ const ProfileView = props => { ...@@ -690,6 +753,10 @@ const ProfileView = props => {
</View> </View>
</ScrollView> </ScrollView>
{/* Image Source Selection Modal */}
{/* Preview Modal */}
{renderPreviewModal()}
{/* Overlay Camera - chỉ UI, mọi handler/state nhận từ props */} {/* Overlay Camera - chỉ UI, mọi handler/state nhận từ props */}
{showCamera && ( {showCamera && (
...@@ -703,27 +770,66 @@ const ProfileView = props => { ...@@ -703,27 +770,66 @@ const ProfileView = props => {
isActive={showCamera} isActive={showCamera}
photo photo
/> />
<View style={styles.controls}> <TouchableOpacity
<TouchableOpacity style={styles.smallBtn} onPress={onToggleCameraPosition}> style={styles.topLeft}
<Text style={styles.btnText}>Đổi cam</Text> onPress={onToggleCameraPosition}>
<Image
source={R.images.icSwitchCamera}
tintColor={R.colors.white}
style={{width: 30, height: 30}}
/>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.shutter} onPress={onTakePhoto} />
<TouchableOpacity style={styles.smallBtn} onPress={onCloseCamera}> <TouchableOpacity style={styles.topRight} onPress={onCloseCamera}>
<Text style={styles.btnText}>Đóng</Text> <Image
source={R.images.icClose}
tintColor={R.colors.white}
style={{width: 30, height: 30}}
/>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.bottomLeft} onPress={onSelectGallery}>
<View style={styles.galleryPreview}>
{recentPhotos.length > 0 ? (
<View style={styles.galleryGrid}>
{recentPhotos.slice(0, 4).map((photoUri, index) => (
<Image
key={index}
source={{uri: photoUri}}
style={styles.galleryThumbnail}
/>
))}
</View>
) : (
<Image
source={R.images.icTakePhoto}
tintColor={R.colors.white}
style={{width: 30, height: 30}}
/>
)}
</View> </View>
</TouchableOpacity>
<TouchableOpacity style={styles.shutter} onPress={onTakePhoto}>
{/* <Image
source={R.images.icTakePhoto}
tintColor={R.colors.white}
style={{width: 24, height: 24}}
/> */}
</TouchableOpacity>
</> </>
) : ( ) : (
<View style={[styles.overlay, styles.center]}> <View style={[styles.overlay, styles.center]}>
<Text style={{ color: '#fff' }}>Không tìm thy camera</Text> <Text style={{color: '#fff'}}>Không tìm thy camera</Text>
<TouchableOpacity style={[styles.smallBtn, { marginTop: 16 }]} onPress={onCloseCamera}> <TouchableOpacity
style={[styles.smallBtn, {marginTop: 16}]}
onPress={onCloseCamera}>
<Text style={styles.btnText}>Đóng</Text> <Text style={styles.btnText}>Đóng</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
</View> </View>
)} )}
</View> </View>
); );
}; };
......
...@@ -7116,6 +7116,11 @@ react-native-image-crop-picker@^0.36.2: ...@@ -7116,6 +7116,11 @@ react-native-image-crop-picker@^0.36.2:
resolved "https://registry.npmjs.org/react-native-image-crop-picker/-/react-native-image-crop-picker-0.36.4.tgz" resolved "https://registry.npmjs.org/react-native-image-crop-picker/-/react-native-image-crop-picker-0.36.4.tgz"
integrity sha512-FOWkYbCEh78V5/aK9HqMSvRnQJtelGwj0UOu1zhE49gO6e4YoKKNBvA15jweAMM/kPA+omDXBIgJaruonoEXGA== integrity sha512-FOWkYbCEh78V5/aK9HqMSvRnQJtelGwj0UOu1zhE49gO6e4YoKKNBvA15jweAMM/kPA+omDXBIgJaruonoEXGA==
react-native-image-picker@^8.2.1:
version "8.2.1"
resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-8.2.1.tgz#1ac7826563cbaa5d5298d9f2acc53c69805e5393"
integrity sha512-FBeGYJGFDjMdGCcyubDJgBAPCQ4L1D3hwLXyUU91jY9ahOZMTbluceVvRmrEKqnDPFJ0gF1NVhJ0nr1nROFLdg==
react-native-indicators@^0.17.0: react-native-indicators@^0.17.0:
version "0.17.0" version "0.17.0"
resolved "https://registry.npmjs.org/react-native-indicators/-/react-native-indicators-0.17.0.tgz" resolved "https://registry.npmjs.org/react-native-indicators/-/react-native-indicators-0.17.0.tgz"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment