import React, { useState, useEffect, useRef, useMemo } from 'react'; import { View, Text, TextInput, Pressable, ScrollView, Keyboard, Platform, Image, // @ts-ignore - Peer dependency } from 'react-native'; // @ts-ignore - Peer dependency import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { GestureDetector, Gesture, Pressable as GHPressable, ScrollView as GHScrollView, // @ts-ignore - Peer dependency } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, runOnJS, withTiming, // @ts-ignore - Peer dependency } from 'react-native-reanimated'; // @ts-ignore - Peer dependency import { CloseIcon } from '../../assets/icons/index.js'; import { FONT_SIZE_MIN, FONT_SIZE_MAX } from '../../constants/dimensions'; import { createTextEditorStyles } from './TextEditorStyles'; import type { TextSegment } from '../../types/segments'; import { deviceUtils } from '../../utils/deviceUtils'; import { useFontFamily } from '../../context/FontFamilyContext'; // Window dimensions removed as centering now uses PREVIEW area constants const hiddenInputStyle = { position: 'absolute' as const, top: -100, width: 0, height: 0, opacity: 0, color: 'transparent', }; type FontSizeSliderProps = { fontSize: number; onFontSizeChange: (size: number) => void; }; const FontSizeSlider: React.FC = ({ fontSize, onFontSizeChange, }) => { const sliderHeight = 150; const minFontSize = FONT_SIZE_MIN; const maxFontSize = FONT_SIZE_MAX; const styles = createTextEditorStyles(); const { fontStyle } = useFontFamily(); const initialY = sliderHeight - ((fontSize - minFontSize) / (maxFontSize - minFontSize)) * sliderHeight; const sliderY = useSharedValue(initialY); const startY = useSharedValue(0); const labelOpacity = useSharedValue(0); useEffect(() => { const newY = sliderHeight - ((fontSize - minFontSize) / (maxFontSize - minFontSize)) * sliderHeight; sliderY.value = withTiming(newY); }, [fontSize, sliderY, minFontSize, maxFontSize]); const sliderGesture = Gesture.Pan() .onStart(() => { startY.value = sliderY.value; labelOpacity.value = withTiming(1); }) .onUpdate((event: any) => { const newY = Math.max( 0, Math.min(sliderHeight, startY.value + event.translationY) ); sliderY.value = newY; const percentage = 1 - newY / sliderHeight; const newFontSize = minFontSize + percentage * (maxFontSize - minFontSize); runOnJS(onFontSizeChange)(Math.round(newFontSize)); }) .onEnd(() => { labelOpacity.value = withTiming(0); }); const thumbStyle = useAnimatedStyle(() => ({ transform: [{ translateY: sliderY.value }], })); const fontSizeIndicatorStyle = useAnimatedStyle(() => ({ transform: [{ translateY: sliderY.value }], })); const labelAnimatedStyle = useAnimatedStyle(() => ({ opacity: labelOpacity.value, })); return ( {maxFontSize} {Math.round(fontSize)} {minFontSize} ); }; type TextEditorProps = { onCancel: () => void; onDone: (textData: Partial & { id?: string | null }) => void; initialTextElement?: TextSegment | null; fontFamily?: string; }; const PressableWrapper = Platform.OS === 'ios' ? Pressable : GHPressable; const RNScrollView = Platform.OS === 'ios' ? ScrollView : GHScrollView; export const TextEditor: React.FC = ({ onCancel, onDone, initialTextElement, fontFamily, }) => { const insets = useSafeAreaInsets(); const [text, setText] = useState(initialTextElement?.text || ''); const [fontSize, setFontSize] = useState( initialTextElement?.fontSize || FONT_SIZE_MIN ); const [textColor, setTextColor] = useState( initialTextElement?.color || 'white' ); const [backgroundColor, setBackgroundColor] = useState( initialTextElement?.backgroundColor || 'transparent' ); const [activeTab, setActiveTab] = useState<'Font' | 'Background'>('Font'); const [isCursorVisible, setIsCursorVisible] = useState(true); const [isKeyboardActive, setIsKeyboardActive] = useState(false); const [keyboardHeight, setKeyboardHeight] = useState(0); const textElementSizeRef = useRef({ width: 0, height: 0 }); const styles = createTextEditorStyles(); const textInputRef = useRef(null); const { fontStyle } = useFontFamily(); useEffect(() => { const keyboardWillShow = Keyboard.addListener( Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', (e: any) => { setIsKeyboardActive(true); setKeyboardHeight(e.endCoordinates.height); } ); const keyboardWillHide = Keyboard.addListener( Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => { setIsKeyboardActive(false); setKeyboardHeight(0); } ); return () => { keyboardWillShow.remove(); keyboardWillHide.remove(); }; }, []); useEffect(() => { let cursorInterval: NodeJS.Timeout | null = null; if (isKeyboardActive) { setIsCursorVisible(true); cursorInterval = setInterval(() => { setIsCursorVisible((prev) => !prev); }, 530); } return () => { if (cursorInterval) { clearInterval(cursorInterval); } }; }, [isKeyboardActive]); useEffect(() => { if (initialTextElement) { setText(initialTextElement.text); setFontSize(initialTextElement.fontSize); setTextColor(initialTextElement.color); setBackgroundColor(initialTextElement.backgroundColor || 'transparent'); } setTimeout(() => (textInputRef.current as any)?.focus(), 100); }, [initialTextElement]); const fontColors = [ '#FFFFFF', // white '#000000', // black '#FF3B30', // red '#FFCC00', // yellow '#4CD964', // green '#007AFF', // blue '#5856D6', // indigo '#FF9500', // orange '#8E8E93', // iOS system gray (neutral) '#34C759', // vibrant green '#AF52DE', // purple '#FF2D55', // pink ]; const backgroundColors = [ 'transparent', // no background '#FFFFFF', // white '#000000', // black '#1C1C1E', // dark gray '#2C2C2C', // deep gray '#F5F5F7', // off-white '#E5E5EA', // light gray '#3A3A3C', // muted dark gray '#FFEBEE', // pastel red '#E8F5E9', // pastel green '#E3F2FD', // pastel blue '#FFFDE7', // pastel yellow ]; const handleDone = () => { // Only calculate default position if editing new text (no existing position) const shouldUseDefaultPosition = !initialTextElement || (initialTextElement.x === undefined && initialTextElement.y === undefined); // If it's a new text or has no position, let DraggableText handle the initial centering // by passing undefined. Otherwise, use the existing position. const defaultX = shouldUseDefaultPosition ? undefined : initialTextElement?.x; const defaultY = shouldUseDefaultPosition ? undefined : initialTextElement?.y; const textData = { id: initialTextElement?.id || null, text: text.trim(), fontSize, color: textColor, backgroundColor, fontFamily, x: defaultX, y: defaultY, }; onDone(textData as Partial & { id?: string | null }); }; const textDisplayStyle = { fontSize: fontSize, color: text ? textColor : 'rgba(255,255,255,0.7)', fontWeight: fontFamily ? ('normal' as const) : ('bold' as const), textAlign: 'center' as const, fontFamily: fontFamily, }; const textBackgroundStyle = { backgroundColor: text.trim() ? backgroundColor : 'transparent', paddingHorizontal: 10, paddingVertical: 5, borderRadius: 5, alignSelf: 'center' as const, maxWidth: '95%', }; const cursorTextStyle = { color: isCursorVisible ? textColor : 'transparent', }; const bottomContainerDynamicStyle = useMemo( () => isKeyboardActive ? { bottom: Platform.OS === 'ios' ? keyboardHeight : 10, } : { bottom: 18 }, [isKeyboardActive, keyboardHeight] ); const bottomContainerIOSPaddingStyle = useMemo( () => Platform.OS === 'ios' && !isKeyboardActive ? { paddingBottom: insets.bottom } : {}, [isKeyboardActive, insets.bottom] ); const getBackgroundColorCircleStyle = (color: string) => ({ backgroundColor: color === 'transparent' ? '#333333' : color, }); return ( Cancel Done { const { width, height } = e.nativeEvent.layout; textElementSizeRef.current = { width, height }; }} style={[ styles.inputContainer, deviceUtils.isAndroid && styles.androidInputContainer, ]} onPress={() => (textInputRef.current as any)?.focus()} > {text || 'Start typing'} {isKeyboardActive && |} {activeTab === 'Font' && ( {fontColors.map((color) => ( setTextColor(color)} style={[ styles.colorCircle, { backgroundColor: color }, textColor === color && styles.selectedColor, ]} /> ))} )} {activeTab === 'Background' && ( {backgroundColors.map((color) => ( setBackgroundColor(color)} style={[ styles.colorCircle, styles.colorCircleCentered, getBackgroundColorCircleStyle(color), backgroundColor === color && styles.selectedColor, ]} > {color === 'transparent' && ( )} ))} )} setActiveTab('Font')}> Font setActiveTab('Background')}> Background ); };