import FontAwesome from '@expo/vector-icons/FontAwesome'; import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; import { useFonts } from 'expo-font'; import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; import * as Notifications from 'expo-notifications'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import 'react-native-reanimated'; import { Animated, AppState, Image, StyleSheet, View } from 'react-native'; import { useColorScheme } from '@/components/useColorScheme'; import { initI18n } from '@/src/i18n'; import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromStorage } from '@/src/modules/dailyWidgetReco'; import { getOrCreateClientUserId } from '@/src/storage/appStorage'; // 配置通知处理方式(即使不发送也建议配置,以确保权限接口正常) Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, // 新版 expo-notifications 类型要求显式返回 banner/list 行为 shouldShowBanner: true, shouldShowList: true, shouldPlaySound: false, shouldSetBadge: false, }), }); export { // Catch any errors thrown by the Layout component. ErrorBoundary, } from 'expo-router'; export const unstable_settings = { // Ensure that reloading on `/modal` keeps a back button present. initialRouteName: 'index', }; // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); export default function RootLayout() { const [loaded, error] = useFonts({ STIXTwoText: require('../assets/fonts/STIXTwoText-VariableFont_wght.ttf'), ...FontAwesome.font, }); const [i18nReady, setI18nReady] = useState(false); const [appReady, setAppReady] = useState(false); const [splashOverlayVisible, setSplashOverlayVisible] = useState(true); const splashOpacity = useRef(new Animated.Value(1)).current; const hasHiddenNativeSplashRef = useRef(false); // Expo Router uses Error Boundaries to catch errors in the navigation tree. useEffect(() => { if (error) throw error; }, [error]); useEffect(() => { initI18n() .catch((e) => { // i18n 初始化失败不应阻塞 App 启动,先打印错误再继续 console.error('i18n init failed', e); }) .finally(() => setI18nReady(true)); }, []); useEffect(() => { // 尽早生成 client_user_id,便于后续任意时刻与后端建立关联(Push Token/偏好等) getOrCreateClientUserId() .then((id) => { if (__DEV__) console.log('[client_user_id]', id); }) .catch((e) => { console.warn('[client_user_id] 生成失败(不阻塞启动)', e); }); }, []); useEffect(() => { // 字体与 i18n 都准备好后,允许渲染 App(原生 splash 的隐藏交给 onLayout,避免“硬切/闪白”) if (loaded && i18nReady) setAppReady(true); }, [loaded, i18nReady]); const onLayoutRootView = useCallback(() => { if (!appReady) return; if (hasHiddenNativeSplashRef.current) return; hasHiddenNativeSplashRef.current = true; // 先隐藏原生 splash,再把同款覆盖层淡出,视觉上实现平滑过渡 void SplashScreen.hideAsync().finally(() => { Animated.timing(splashOpacity, { toValue: 0, duration: 380, useNativeDriver: true, }).start(({ finished }) => { if (finished) setSplashOverlayVisible(false); }); }); }, [appReady, splashOpacity]); const content = useMemo(() => { if (!appReady) return null; return ; }, [appReady]); if (!loaded || !i18nReady) { return null; } return ( {content} {splashOverlayVisible && ( )} ); } function RootLayoutNav() { const colorScheme = useColorScheme(); useEffect(() => { // iOS 小组件:启动时把必要信息写入共享区,并尽力刷新一次“每日推荐” syncWidgetConfig().catch(() => {}); syncWidgetUserProfileFromStorage().catch(() => {}); ensureDailyWidgetRecoUpToDate({ reason: 'app_start' }).catch(() => {}); const sub = AppState.addEventListener('change', (state) => { if (state === 'active') { // App 回到前台时尝试刷新(失败不阻塞) ensureDailyWidgetRecoUpToDate({ reason: 'app_active' }).catch(() => {}); } }); return () => sub.remove(); }, []); return ( {/* 启动分发页:根据 onboarding 状态跳转 */} {/* Onboarding 分组 */} {/* 主应用分组(不使用 Tabs) */} {/* 其他 */} ); } const styles = StyleSheet.create({ root: { flex: 1, }, splashOverlay: { flex: 1, alignItems: 'center', justifyContent: 'center', // 与 app.json 的 expo.splash.backgroundColor 保持一致 backgroundColor: '#EAD2BA', }, splashImage: { width: '80%', height: '80%', }, });