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'),
......
...@@ -56,4 +56,4 @@ export const TRASHEMAIL = 'TRASHEMAIL'; ...@@ -56,4 +56,4 @@ export const TRASHEMAIL = 'TRASHEMAIL';
export const LISTROLLCALL = 'LISTROLLCALL'; export const LISTROLLCALL = 'LISTROLLCALL';
export const LISTSTUDENTROLLCALL = 'LISTSTUDENTROLLCALL'; export const LISTSTUDENTROLLCALL = 'LISTSTUDENTROLLCALL';
export const DETAILROLLCALL = 'DETAILROLLCALL'; export const DETAILROLLCALL = 'DETAILROLLCALL';
\ No newline at end of file
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,36 +16,46 @@ const Tab = createBottomTabNavigator(); ...@@ -15,36 +16,46 @@ 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={{
headerShown: false,
tabBarStyle: hideTabBar
? {display: 'none'}
: {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.29,
shadowRadius: 2,
elevation: 7,
justifyContent: 'center',
},
}}
tabBarOptions={{ tabBarOptions={{
showIcon: true, showIcon: true,
showLabel: true, showLabel: true,
activeTintColor: R.colors.main, activeTintColor: R.colors.main,
style: {
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.29,
shadowRadius: 2,
elevation: 7,
justifyContent: 'center',
},
}}> }}>
<Tab.Screen <Tab.Screen
name="HomeScreen1" name="HomeScreen1"
...@@ -59,7 +70,7 @@ const TabNavigator = props => { ...@@ -59,7 +70,7 @@ const TabNavigator = props => {
), ),
}} }}
/> />
<Tab.Screen <Tab.Screen
name="HomeScreen145" name="HomeScreen145"
component={DrawerNavigator} component={DrawerNavigator}
options={{ options={{
......
import React, { useRef, useState } from 'react'; import React, {useRef, useState, useEffect} from 'react';
import { Platform } from 'react-native'; import {Platform, DeviceEventEmitter, Alert, PermissionsAndroid} from 'react-native';
import {launchImageLibrary} from 'react-native-image-picker';
import {CameraRoll} from '@react-native-camera-roll/camera-roll';
import ProfileView from './view'; import ProfileView from './view';
import { useCameraDevice, useCameraPermission } from 'react-native-vision-camera'; import {useCameraDevice, useCameraPermission} from 'react-native-vision-camera';
const Profile = () => { const Profile = (props) => {
// --- mock profile ---
const [user] = useState({ const [user] = useState({
name: 'Phạm Minh Quân', name: 'Phạm Minh Quân',
role: 'Trợ lý bộ môn thuộc trường', role: 'Trợ lý bộ môn thuộc trường',
...@@ -26,7 +27,6 @@ const Profile = () => { ...@@ -26,7 +27,6 @@ const Profile = () => {
workCommencementDate: '20/03/2025', workCommencementDate: '20/03/2025',
}); });
// --- form states ---
const [phoneNumber, setPhoneNumber] = useState(''); const [phoneNumber, setPhoneNumber] = useState('');
const [oldTeacherCode, setOldTeacherCode] = useState(''); const [oldTeacherCode, setOldTeacherCode] = useState('');
const [workPlace, setWorkPlace] = useState(''); const [workPlace, setWorkPlace] = useState('');
...@@ -63,25 +63,95 @@ const Profile = () => { ...@@ -63,25 +63,95 @@ const Profile = () => {
// --- radio group --- // --- radio group ---
const [selectedValue2, setSelectedValue2] = useState('1'); const [selectedValue2, setSelectedValue2] = useState('1');
const options2 = [ const options2 = [
{ label: 'Cơ chế', value: '1' }, {label: 'Cơ chế', value: '1'},
{ label: 'Biên hữu', value: '2' }, {label: 'Biên hữu', value: '2'},
]; ];
const onValueChange2 = (v) => setSelectedValue2(v); const onValueChange2 = v => setSelectedValue2(v);
// --- camera logic (container) --- // --- camera logic (container) ---
const [showCamera, setShowCamera] = useState(false); const [showCamera, setShowCamera] = useState(false);
const [showImageSourceModal, setShowImageSourceModal] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [avatarUri, setAvatarUri] = useState(null); const [avatarUri, setAvatarUri] = useState(null);
const [previewUri, setPreviewUri] = useState(null);
const [recentPhotos, setRecentPhotos] = useState([]);
const [useFront, setUseFront] = useState(false); const [useFront, setUseFront] = useState(false);
const cameraRef = useRef(null); const cameraRef = useRef(null);
const device = useCameraDevice(useFront ? 'front' : 'back'); const device = useCameraDevice(useFront ? 'front' : 'back');
const { hasPermission, requestPermission } = useCameraPermission(); const {hasPermission, requestPermission} = useCameraPermission();
// Load recent photos for gallery preview
const loadRecentPhotos = async () => {
try {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
return;
}
}
const photos = await CameraRoll.getPhotos({
first: 4, // Get 4 recent photos for preview
assetType: 'Photos',
});
setRecentPhotos(photos.edges.map(edge => edge.node.image.uri));
} catch (error) {
console.log('Error loading recent photos:', error);
}
};
useEffect(() => {
loadRecentPhotos();
}, []);
const openImageSourceModal = async () => {
// Go directly to camera instead of showing modal
if (!hasPermission) {
const ok = await requestPermission();
if (!ok) return;
}
setShowCamera(true);
DeviceEventEmitter.emit('hideTabs', true);
};
const closeImageSourceModal = () => {
setShowImageSourceModal(false);
};
const openCamera = async () => { const openCamera = async () => {
if (!hasPermission) { if (!hasPermission) {
const ok = await requestPermission(); const ok = await requestPermission();
if (!ok) return; if (!ok) return;
} }
setShowImageSourceModal(false);
setShowCamera(true); setShowCamera(true);
DeviceEventEmitter.emit('hideTabs', true);
};
const openGallery = () => {
const options = {
mediaType: 'photo',
includeBase64: false,
maxHeight: 2000,
maxWidth: 2000,
};
launchImageLibrary(options, response => {
if (response.didCancel || response.error) {
return;
}
if (response.assets && response.assets[0]) {
const imageUri = response.assets[0].uri;
setPreviewUri(imageUri);
setShowImageSourceModal(false);
setShowPreview(true);
DeviceEventEmitter.emit('hideTabs', true);
}
});
}; };
const onTakePhoto = async () => { const onTakePhoto = async () => {
...@@ -91,12 +161,38 @@ const Profile = () => { ...@@ -91,12 +161,38 @@ const Profile = () => {
flash: 'off', flash: 'off',
}); });
const uri = Platform.OS === 'android' ? 'file://' + photo.path : photo.path; const uri = Platform.OS === 'android' ? 'file://' + photo.path : photo.path;
setAvatarUri(uri); setPreviewUri(uri);
setShowCamera(false);
setShowPreview(true);
// Keep tabs hidden for preview
};
const onToggleCameraPosition = () => setUseFront(v => !v);
const onCloseCamera = () => {
setShowCamera(false); setShowCamera(false);
// Show tab bar when camera closes
DeviceEventEmitter.emit('hideTabs', false);
};
const onConfirmPhoto = () => {
setAvatarUri(previewUri);
setPreviewUri(null);
setShowPreview(false);
DeviceEventEmitter.emit('hideTabs', false);
};
const onRetakePhoto = () => {
setPreviewUri(null);
setShowPreview(false);
DeviceEventEmitter.emit('hideTabs', false);
}; };
const onToggleCameraPosition = () => setUseFront((v) => !v); const onClosePreview = () => {
const onCloseCamera = () => setShowCamera(false); setPreviewUri(null);
setShowPreview(false);
DeviceEventEmitter.emit('hideTabs', false);
};
// --- save handler --- // --- save handler ---
const handleSave = () => { const handleSave = () => {
...@@ -145,56 +241,97 @@ const Profile = () => { ...@@ -145,56 +241,97 @@ const Profile = () => {
<ProfileView <ProfileView
// profile // profile
dataProfile={user} dataProfile={user}
// camera props (UI sẽ dùng để hiển thị) // camera props (UI sẽ dùng để hiển thị)
avatarUri={avatarUri} avatarUri={avatarUri}
onPressSelectImage={openCamera} onPressSelectImage={openImageSourceModal}
showCamera={showCamera} showCamera={showCamera}
cameraRef={cameraRef} cameraRef={cameraRef}
device={device} device={device}
onToggleCameraPosition={onToggleCameraPosition} onToggleCameraPosition={onToggleCameraPosition}
onTakePhoto={onTakePhoto} onTakePhoto={onTakePhoto}
onCloseCamera={onCloseCamera} onCloseCamera={onCloseCamera}
// image source modal
showImageSourceModal={showImageSourceModal}
onCloseImageSourceModal={closeImageSourceModal}
onSelectCamera={openCamera}
onSelectGallery={openGallery}
// preview
showPreview={showPreview}
previewUri={previewUri}
onConfirmPhoto={onConfirmPhoto}
onRetakePhoto={onRetakePhoto}
onClosePreview={onClosePreview}
// gallery preview
recentPhotos={recentPhotos}
// radio // radio
selectedValue2={selectedValue2} selectedValue2={selectedValue2}
options2={options2} options2={options2}
onValueChange2={onValueChange2} onValueChange2={onValueChange2}
// form states // form states
phoneNumber={phoneNumber} setPhoneNumber={setPhoneNumber} phoneNumber={phoneNumber}
oldTeacherCode={oldTeacherCode} setOldTeacherCode={setOldTeacherCode} setPhoneNumber={setPhoneNumber}
workPlace={workPlace} setWorkPlace={setWorkPlace} oldTeacherCode={oldTeacherCode}
position={position} setPosition={setPosition} setOldTeacherCode={setOldTeacherCode}
extraDuty={extraDuty} setExtraDuty={setExtraDuty} workPlace={workPlace}
laborType={laborType} setLaborType={setLaborType} setWorkPlace={setWorkPlace}
employmentType={employmentType} setEmploymentType={setEmploymentType} position={position}
appointmentDecision={appointmentDecision} setAppointmentDecision={setAppointmentDecision} setPosition={setPosition}
appointmentDate={appointmentDate} setAppointmentDate={setAppointmentDate} extraDuty={extraDuty}
issuingAgency={issuingAgency} setIssuingAgency={setIssuingAgency} setExtraDuty={setExtraDuty}
jobBeforeRecruitment={jobBeforeRecruitment} setJobBeforeRecruitment={setJobBeforeRecruitment} laborType={laborType}
allowanceSeniorityDate={allowanceSeniorityDate} setAllowanceSeniorityDate={setAllowanceSeniorityDate} setLaborType={setLaborType}
workAllowancePercent={workAllowancePercent} setWorkAllowancePercent={setWorkAllowancePercent} employmentType={employmentType}
teacherAllowancePercent={teacherAllowancePercent} setTeacherAllowancePercent={setTeacherAllowancePercent} setEmploymentType={setEmploymentType}
teacherType={teacherType} setTeacherType={setTeacherType} appointmentDecision={appointmentDecision}
academicUnit={academicUnit} setAcademicUnit={setAcademicUnit} setAppointmentDecision={setAppointmentDecision}
subjectDepartment={subjectDepartment} setSubjectDepartment={setSubjectDepartment} appointmentDate={appointmentDate}
employmentStatus={employmentStatus} setEmploymentStatus={setEmploymentStatus} setAppointmentDate={setAppointmentDate}
workStartDate={workStartDate} setWorkStartDate={setWorkStartDate} issuingAgency={issuingAgency}
currentContractType={currentContractType} setCurrentContractType={setCurrentContractType} setIssuingAgency={setIssuingAgency}
contractStartDate={contractStartDate} setContractStartDate={setContractStartDate} jobBeforeRecruitment={jobBeforeRecruitment}
contractEndDate={contractEndDate} setContractEndDate={setContractEndDate} setJobBeforeRecruitment={setJobBeforeRecruitment}
currentContractNumber={currentContractNumber} setCurrentContractNumber={setCurrentContractNumber} allowanceSeniorityDate={allowanceSeniorityDate}
contractEffectiveDate={contractEffectiveDate} setContractEffectiveDate={setContractEffectiveDate} setAllowanceSeniorityDate={setAllowanceSeniorityDate}
fullTimeWorkStartDate={fullTimeWorkStartDate} setFullTimeWorkStartDate={setFullTimeWorkStartDate} workAllowancePercent={workAllowancePercent}
lastClassificationDate={lastClassificationDate} setLastClassificationDate={setLastClassificationDate} setWorkAllowancePercent={setWorkAllowancePercent}
currentWorkAllowancePercent={currentWorkAllowancePercent} setCurrentWorkAllowancePercent={setCurrentWorkAllowancePercent} teacherAllowancePercent={teacherAllowancePercent}
salaryGrade={salaryGrade} setSalaryGrade={setSalaryGrade} setTeacherAllowancePercent={setTeacherAllowancePercent}
currentSalaryLevel={currentSalaryLevel} setCurrentSalaryLevel={setCurrentSalaryLevel} teacherType={teacherType}
currentSalaryCoefficient={currentSalaryCoefficient} setCurrentSalaryCoefficient={setCurrentSalaryCoefficient} setTeacherType={setTeacherType}
salaryIncreaseMilestone={salaryIncreaseMilestone} setSalaryIncreaseMilestone={setSalaryIncreaseMilestone} academicUnit={academicUnit}
lecturerTitle={lecturerTitle} setLecturerTitle={setLecturerTitle} setAcademicUnit={setAcademicUnit}
subjectDepartment={subjectDepartment}
setSubjectDepartment={setSubjectDepartment}
employmentStatus={employmentStatus}
setEmploymentStatus={setEmploymentStatus}
workStartDate={workStartDate}
setWorkStartDate={setWorkStartDate}
currentContractType={currentContractType}
setCurrentContractType={setCurrentContractType}
contractStartDate={contractStartDate}
setContractStartDate={setContractStartDate}
contractEndDate={contractEndDate}
setContractEndDate={setContractEndDate}
currentContractNumber={currentContractNumber}
setCurrentContractNumber={setCurrentContractNumber}
contractEffectiveDate={contractEffectiveDate}
setContractEffectiveDate={setContractEffectiveDate}
fullTimeWorkStartDate={fullTimeWorkStartDate}
setFullTimeWorkStartDate={setFullTimeWorkStartDate}
lastClassificationDate={lastClassificationDate}
setLastClassificationDate={setLastClassificationDate}
currentWorkAllowancePercent={currentWorkAllowancePercent}
setCurrentWorkAllowancePercent={setCurrentWorkAllowancePercent}
salaryGrade={salaryGrade}
setSalaryGrade={setSalaryGrade}
currentSalaryLevel={currentSalaryLevel}
setCurrentSalaryLevel={setCurrentSalaryLevel}
currentSalaryCoefficient={currentSalaryCoefficient}
setCurrentSalaryCoefficient={setCurrentSalaryCoefficient}
salaryIncreaseMilestone={salaryIncreaseMilestone}
setSalaryIncreaseMilestone={setSalaryIncreaseMilestone}
lecturerTitle={lecturerTitle}
setLecturerTitle={setLecturerTitle}
// actions // actions
onSave={handleSave} onSave={handleSave}
/> />
......
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,13 +13,11 @@ import R from '../../assets/R'; ...@@ -12,13 +13,11 @@ 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,
// camera // camera
avatarUri, avatarUri,
onPressSelectImage, onPressSelectImage,
...@@ -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,17 +133,69 @@ const ProfileView = props => { ...@@ -122,17 +133,69 @@ 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}>
<View style={styles.headerLeft}> <View style={styles.headerLeft}>
<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>
</View> </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}>
</TouchableOpacity> <Image
<TouchableOpacity style={styles.shutter} onPress={onTakePhoto} /> source={R.images.icSwitchCamera}
<TouchableOpacity style={styles.smallBtn} onPress={onCloseCamera}> tintColor={R.colors.white}
<Text style={styles.btnText}>Đóng</Text> style={{width: 30, height: 30}}
</TouchableOpacity> />
</View> </TouchableOpacity>
<TouchableOpacity style={styles.topRight} onPress={onCloseCamera}>
<Image
source={R.images.icClose}
tintColor={R.colors.white}
style={{width: 30, height: 30}}
/>
</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>
</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