import { Ionicons } from "@expo/vector-icons"; import { BlurView } from "expo-blur"; import { useAtomValue } from "jotai"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Animated, Easing, Pressable, ScrollView, TVFocusGuideView, } from "react-native"; import { Text } from "@/components/common/Text"; import { TVUserCard } from "@/components/tv/TVUserCard"; import { useScaledTVTypography } from "@/constants/TVTypography"; import useRouter from "@/hooks/useAppRouter"; import { tvAccountSelectModalAtom } from "@/utils/atoms/tvAccountSelectModal"; import { store } from "@/utils/store"; // Action button for bottom sheet const TVAccountSelectAction: React.FC<{ label: string; icon: keyof typeof Ionicons.glyphMap; variant?: "default" | "destructive"; onPress: () => void; }> = ({ label, icon, variant = "default", onPress }) => { const [focused, setFocused] = useState(false); const scale = useRef(new Animated.Value(1)).current; const typography = useScaledTVTypography(); const animateTo = (v: number) => Animated.timing(scale, { toValue: v, duration: 150, easing: Easing.out(Easing.quad), useNativeDriver: true, }).start(); const isDestructive = variant === "destructive"; return ( { setFocused(true); animateTo(1.05); }} onBlur={() => { setFocused(false); animateTo(1); }} > {label} ); }; export default function TVAccountSelectModalPage() { const typography = useScaledTVTypography(); const router = useRouter(); const modalState = useAtomValue(tvAccountSelectModalAtom); const { t } = useTranslation(); const [isReady, setIsReady] = useState(false); const overlayOpacity = useRef(new Animated.Value(0)).current; const sheetTranslateY = useRef(new Animated.Value(300)).current; // Animate in on mount useEffect(() => { overlayOpacity.setValue(0); sheetTranslateY.setValue(300); Animated.parallel([ Animated.timing(overlayOpacity, { toValue: 1, duration: 250, easing: Easing.out(Easing.quad), useNativeDriver: true, }), Animated.timing(sheetTranslateY, { toValue: 0, duration: 300, easing: Easing.out(Easing.cubic), useNativeDriver: true, }), ]).start(); const timer = setTimeout(() => setIsReady(true), 100); return () => { clearTimeout(timer); store.set(tvAccountSelectModalAtom, null); }; }, [overlayOpacity, sheetTranslateY]); if (!modalState) { return null; } return ( {/* Title */} {t("server.select_account")} {/* Server name as subtitle */} {modalState.server.name || modalState.server.address} {/* All options in single horizontal row */} {isReady && ( {modalState.server.accounts?.map((account, index) => ( { modalState.onAccountAction(account); }} hasTVPreferredFocus={index === 0} /> ))} { modalState.onAddAccount(); router.back(); }} /> { modalState.onDeleteServer(); router.back(); }} /> )} ); }