import { useRef, useState } from 'react'; import { View, Text, Image, Modal, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, } from 'react-native'; import { GestureDetector, Gesture, GestureHandlerRootView, } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; import { manipulateAsync, SaveFormat } from 'expo-image-manipulator'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTranslation } from 'react-i18next'; import { useColors } from '../../lib/theme'; import { AppAlert } from '../AppAlert'; const CROP_SIZE = Math.min(Dimensions.get('window').width - 48, 320); const SPRING = { damping: 18, stiffness: 200 }; type Props = { imageUri: string | null; onConfirm: (croppedUri: string) => void; onCancel: () => void; }; export function AvatarCropSheet({ imageUri, onConfirm, onCancel }: Props) { const { t } = useTranslation(); const colors = useColors(); const insets = useSafeAreaInsets(); const [processing, setProcessing] = useState(false); const [uploadError, setUploadError] = useState(null); const scale = useSharedValue(1); const savedScale = useSharedValue(1); const translateX = useSharedValue(0); const translateY = useSharedValue(0); const savedTranslateX = useSharedValue(0); const savedTranslateY = useSharedValue(0); const pinch = Gesture.Pinch() .onUpdate((e) => { scale.value = Math.min(Math.max(savedScale.value * e.scale, 1), 5); }) .onEnd(() => { savedScale.value = scale.value; }); const pan = Gesture.Pan() .onUpdate((e) => { translateX.value = savedTranslateX.value + e.translationX; translateY.value = savedTranslateY.value + e.translationY; }) .onEnd(() => { savedTranslateX.value = translateX.value; savedTranslateY.value = translateY.value; }); const composed = Gesture.Simultaneous(pinch, pan); const imageStyle = useAnimatedStyle(() => ({ transform: [ { translateX: translateX.value }, { translateY: translateY.value }, { scale: scale.value }, ], })); function reset() { scale.value = withSpring(1, SPRING); savedScale.value = 1; translateX.value = withSpring(0, SPRING); translateY.value = withSpring(0, SPRING); savedTranslateX.value = 0; savedTranslateY.value = 0; } async function handleConfirm() { if (!imageUri || processing) return; setProcessing(true); try { const result = await manipulateAsync( imageUri, [{ resize: { width: 512, height: 512 } }], { format: SaveFormat.JPEG, compress: 0.7 }, ); onConfirm(result.uri); } catch (e: any) { setUploadError(e?.message ?? t('alert.error_generic')); } finally { setProcessing(false); } } return ( {t('common.cancel')} {t('profile.crop_title')} {processing ? ( ) : ( {t('profile.crop_confirm')} )} {imageUri ? ( ) : null} {t('profile.crop_hint')} {t('profile.crop_reset')} setUploadError(null)} /> ); } const CORNER = 18; const BORDER = 2.5; const styles = StyleSheet.create({ container: { flex: 1, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: StyleSheet.hairlineWidth, }, headerTitle: { fontSize: 16, fontFamily: 'Nunito_700Bold', }, headerBtn: { minWidth: 64, alignItems: 'center', }, headerBtnText: { fontSize: 15, fontFamily: 'Nunito_600SemiBold', }, body: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 24, gap: 20, }, cropFrame: { borderRadius: 12, overflow: 'hidden', position: 'relative', backgroundColor: '#111', }, cropOverflow: { width: '100%', height: '100%', overflow: 'hidden', }, cornerTL: { position: 'absolute', top: 0, left: 0, width: CORNER, height: CORNER, borderTopWidth: BORDER, borderLeftWidth: BORDER, borderColor: '#fff', borderTopLeftRadius: 12, }, cornerTR: { position: 'absolute', top: 0, right: 0, width: CORNER, height: CORNER, borderTopWidth: BORDER, borderRightWidth: BORDER, borderColor: '#fff', borderTopRightRadius: 12, }, cornerBL: { position: 'absolute', bottom: 0, left: 0, width: CORNER, height: CORNER, borderBottomWidth: BORDER, borderLeftWidth: BORDER, borderColor: '#fff', borderBottomLeftRadius: 12, }, cornerBR: { position: 'absolute', bottom: 0, right: 0, width: CORNER, height: CORNER, borderBottomWidth: BORDER, borderRightWidth: BORDER, borderColor: '#fff', borderBottomRightRadius: 12, }, hint: { fontSize: 12, fontFamily: 'Nunito_400Regular', textAlign: 'center', lineHeight: 17, }, resetBtn: { paddingVertical: 8, paddingHorizontal: 16, }, resetText: { fontSize: 13, fontFamily: 'Nunito_600SemiBold', }, });