import './gesture-handler' import React, { useEffect, useRef, useState } from 'react' import 'react-native-url-polyfill/auto' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import Jellify from './src/components/jellify' import { TamaguiProvider } from 'tamagui' import { LogBox, Platform, useColorScheme } from 'react-native' import jellifyConfig from './tamagui.config' import { queryClientPersister } from './src/constants/storage' import { ONE_DAY, queryClient } from './src/constants/query-client' import { GestureHandlerRootView } from 'react-native-gesture-handler' import TrackPlayer, { AndroidAudioContentType, AppKilledPlaybackBehavior, IOSCategory, IOSCategoryOptions, } from 'react-native-track-player' import { CAPABILITIES } from './src/player/constants' import { SafeAreaProvider } from 'react-native-safe-area-context' import { NavigationContainer } from '@react-navigation/native' import { JellifyDarkTheme, JellifyLightTheme, JellifyOLEDTheme } from './src/components/theme' import { requestStoragePermission } from './src/utils/permisson-helpers' import ErrorBoundary from './src/components/ErrorBoundary' import OTAUpdateScreen from './src/components/OtaUpdates' import { usePerformanceMonitor } from './src/hooks/use-performance-monitor' import navigationRef from './navigation' import { BUFFERS, PROGRESS_UPDATE_EVENT_INTERVAL } from './src/player/config' import { useThemeSetting } from './src/stores/settings/app' LogBox.ignoreAllLogs() export default function App(): React.JSX.Element { // Add performance monitoring to track app-level re-renders const performanceMetrics = usePerformanceMonitor('App', 3) const [playerIsReady, setPlayerIsReady] = useState(false) const playerInitializedRef = useRef(false) useEffect(() => { // Guard against double initialization (React StrictMode, hot reload) if (playerInitializedRef.current) return playerInitializedRef.current = true TrackPlayer.setupPlayer({ autoHandleInterruptions: true, iosCategory: IOSCategory.Playback, iosCategoryOptions: [ IOSCategoryOptions.AllowAirPlay, IOSCategoryOptions.AllowBluetooth, ], androidAudioContentType: AndroidAudioContentType.Music, minBuffer: 30, // 30 seconds minimum buffer ...BUFFERS, }) .then(() => TrackPlayer.updateOptions({ capabilities: CAPABILITIES, notificationCapabilities: CAPABILITIES, // Reduced interval for smoother progress tracking and earlier prefetch detection progressUpdateEventInterval: PROGRESS_UPDATE_EVENT_INTERVAL, // Stop playback and remove notification when app is killed to prevent battery drain android: { appKilledPlaybackBehavior: AppKilledPlaybackBehavior.StopPlaybackAndRemoveNotification, }, }), ) .catch((error) => { // Player may already be initialized (e.g., after hot reload) // This is expected and not a fatal error console.log('[TrackPlayer] Setup caught:', error?.message ?? error) }) .finally(() => { setPlayerIsReady(true) requestStoragePermission() }) }, []) // Empty deps - only run once on mount const [reloader, setReloader] = useState(0) const handleRetry = () => setReloader((r) => r + 1) return ( ) } function Container({ playerIsReady }: { playerIsReady: boolean }): React.JSX.Element { const [theme] = useThemeSetting() const isDarkMode = useColorScheme() === 'dark' return ( {playerIsReady && } ) }