import { useEffect, useRef } from 'react'; import { Animated, Easing, Text, View } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useColors } from '../../lib/theme'; /** * "Tap-Here"-Indicator UNTER einem Screenshot. * * Vorher: absolut-positionierter Pulse-Ring INNERHALB des Screenshots * → musste per-locale fine-tuned werden weil Apple-Dialog-Dimensionen * pro Sprache variieren (DE-Text länger als EN, Modal höher, Button * rutscht runter…). * * Jetzt: render-unter dem Screenshot. Layout-agnostic. Animation lenkt * Aufmerksamkeit auf die richtige Region ohne pixel-genaue Position. * * ┌──────────────────────────┐ * │ │ * │ [Nicht erlauben] │ * │ [Erlauben] │ * └──────────────────────────┘ * ▲ * ┌─────────────┐ * │ ▲ Tippe │ ← Animated bouncing pill mit Pfeil + Label * │ "Erlauben" │ * └─────────────┘ */ export function ScreenshotPointer({ buttonLabel, alignment = 'center', }: { /** Der Text auf dem korrekten Button im iOS-Dialog. Wird ins Label übernommen. */ buttonLabel: string; /** * Horizontale Position des Pointer-Pakets relativ zum Screenshot. * 'center' = unter zentrierten Buttons (z.B. iOS NEFilter "Erlauben" unten). * 'left' = unter links-positioniertem Button (z.B. iOS Family Controls * "Fortfahren"/"Continuer" — Apple platziert die Zustimmung * links-grau, Decline rechts-blau bei Screen-Time-Permission). * 'right' = unter rechts-positioniertem Button (symmetrisch). */ alignment?: 'left' | 'center' | 'right'; }) { const colors = useColors(); // Fixed Offset in dp — auf einem 390pt-iPhone shiftet das ~80pt vom Mittel- // punkt weg, das matched die Button-Position innerhalb des ~80%-breiten // System-Dialog-Modals gut genug ohne pixel-genaue Kalibrierung. const offsetX = alignment === 'left' ? -80 : alignment === 'right' ? 80 : 0; const bounce = useRef(new Animated.Value(0)).current; const pulse = useRef(new Animated.Value(0)).current; useEffect(() => { const bounceLoop = Animated.loop( Animated.sequence([ Animated.timing(bounce, { toValue: 1, duration: 600, useNativeDriver: true, easing: Easing.out(Easing.cubic), }), Animated.timing(bounce, { toValue: 0, duration: 600, useNativeDriver: true, easing: Easing.in(Easing.cubic), }), ]), ); const pulseLoop = Animated.loop( Animated.sequence([ Animated.timing(pulse, { toValue: 1, duration: 900, useNativeDriver: true }), Animated.timing(pulse, { toValue: 0, duration: 900, useNativeDriver: true }), ]), ); bounceLoop.start(); pulseLoop.start(); return () => { bounceLoop.stop(); pulseLoop.stop(); }; }, [bounce, pulse]); const translateY = bounce.interpolate({ inputRange: [0, 1], outputRange: [0, -6] }); const arrowOpacity = pulse.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1] }); const arrowScale = pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 1.12] }); return ( {/* Animated Up-Arrow zeigt auf den Screenshot (auf dessen relevanten Button) */} {/* Label-Pille */} 👆 {buttonLabel} ); }