Files
digital-pilates/app/_layout.tsx

184 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import 'react-native-reanimated';
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useQuickActions } from '@/hooks/useQuickActions';
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
import { notificationService } from '@/services/notifications';
import { setupQuickActions } from '@/services/quickActions';
import { initializeWaterRecordBridge } from '@/services/waterRecordBridge';
import { WaterRecordSource } from '@/services/waterRecords';
import { store } from '@/store';
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
import { createWaterRecordAction } from '@/store/waterSlice';
import { DailySummaryNotificationHelpers, MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
import React from 'react';
import { DialogProvider } from '@/components/ui/DialogProvider';
import { ToastProvider } from '@/contexts/ToastContext';
import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
import { Provider } from 'react-redux';
let resolver: (() => void) | null;
// Create a promise and store its resolve function for later
const promise = new Promise<void>((resolve) => {
resolver = resolve;
});
function Bootstrapper({ children }: { children: React.ReactNode }) {
const dispatch = useAppDispatch();
const { privacyAgreed, profile } = useAppSelector((state) => state.user);
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
const [userDataLoaded, setUserDataLoaded] = React.useState(false);
// 初始化快捷动作处理
useQuickActions();
React.useEffect(() => {
const loadUserData = async () => {
await dispatch(rehydrateUser());
setUserDataLoaded(true);
};
const initializeNotifications = async () => {
try {
await BackgroundTaskManager.getInstance().initialize(promise);
// 初始化通知服务
await notificationService.initialize();
console.log('通知服务初始化成功');
// 注册午餐提醒12:00
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '');
console.log('午餐提醒已注册');
// 注册晚餐提醒18:00
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '');
console.log('晚餐提醒已注册');
// 注册心情提醒21:00
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
console.log('心情提醒已注册');
await DailySummaryNotificationHelpers.scheduleDailySummaryNotification(profile.name || '')
// 初始化快捷动作
await setupQuickActions();
console.log('快捷动作初始化成功');
// 初始化喝水记录 bridge
initializeWaterRecordBridge();
console.log('喝水记录 Bridge 初始化成功');
// 检查并同步Widget数据更改
const widgetSync = await syncPendingWidgetChanges();
if (widgetSync.hasPendingChanges && widgetSync.pendingRecords) {
console.log(`检测到 ${widgetSync.pendingRecords.length} 条待同步的水记录`);
// 将待同步的记录添加到 Redux store
for (const record of widgetSync.pendingRecords) {
try {
await store.dispatch(createWaterRecordAction({
amount: record.amount,
recordedAt: record.recordedAt,
source: WaterRecordSource.Auto, // 标记为自动添加来自Widget
})).unwrap();
console.log(`成功同步水记录: ${record.amount}ml at ${record.recordedAt}`);
} catch (error) {
console.error('同步水记录失败:', error);
}
}
// 清除已同步的记录
await clearPendingWaterRecords();
console.log('所有待同步的水记录已处理完成');
}
} catch (error) {
console.error('通知服务、后台任务管理器或快捷动作初始化失败:', error);
}
};
loadUserData();
initializeNotifications();
// 冷启动时清空 AI 教练会话缓存
clearAiCoachSessionCache();
}, [dispatch]);
React.useEffect(() => {
// 当用户数据加载完成后,检查是否需要显示隐私同意弹窗
if (userDataLoaded && !privacyAgreed) {
setShowPrivacyModal(true);
}
}, [userDataLoaded, privacyAgreed]);
const handlePrivacyAgree = () => {
dispatch(setPrivacyAgreed());
setShowPrivacyModal(false);
};
const handlePrivacyDisagree = () => {
// RNExitApp.exitApp();
};
return (
<DialogProvider>
{children}
<PrivacyConsentModal
visible={showPrivacyModal}
onAgree={handlePrivacyAgree}
onDisagree={handlePrivacyDisagree}
/>
</DialogProvider>
);
}
export default function RootLayout() {
const [loaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
});
if (!loaded) {
// Async font loading only occurs in development.
return null;
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Provider store={store}>
<Bootstrapper>
<ToastProvider>
<ThemeProvider value={DefaultTheme}>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="challenge" options={{ headerShown: false }} />
<Stack.Screen name="training-plan" options={{ headerShown: false }} />
<Stack.Screen name="workout" options={{ headerShown: false }} />
<Stack.Screen name="profile/edit" />
<Stack.Screen name="profile/goals" options={{ headerShown: false }} />
<Stack.Screen name="goals-list" options={{ headerShown: false }} />
<Stack.Screen name="ai-posture-assessment" />
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
<Stack.Screen name="water-settings" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="dark" />
</ThemeProvider>
</ToastProvider>
</Bootstrapper>
</Provider>
</GestureHandlerRootView>
);
}