import 'dart:async'; import 'package:facebook_app_events/facebook_app_events.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'core/auth/auth_service.dart'; import 'core/nav/app_route_observer.dart'; import 'core/config/facebook_config.dart'; import 'core/log/app_logger.dart'; import 'core/theme/app_colors.dart'; import 'core/theme/app_theme.dart'; import 'core/user/user_state.dart'; import 'features/gallery/gallery_screen.dart'; import 'features/generate_video/generate_progress_screen.dart'; import 'features/generate_video/generate_video_screen.dart'; import 'features/gallery/models/gallery_task_item.dart'; import 'features/generate_video/generation_result_screen.dart'; import 'features/home/home_playback_resume.dart'; import 'features/home/home_screen.dart'; import 'features/home/models/task_item.dart'; import 'features/profile/profile_screen.dart'; import 'features/recharge/recharge_screen.dart'; import 'shared/widgets/bottom_nav_bar.dart'; import 'shared/tab_selector_scope.dart'; /// Root app widget with navigation class App extends StatefulWidget { const App({super.key}); @override State createState() => _AppState(); } class _AppState extends State with WidgetsBindingObserver { NavTab _currentTab = NavTab.home; static final _fbLog = AppLogger('FB'); final FacebookAppEvents _fbAppEvents = FacebookAppEvents(); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); // Cold start is often already resumed, so lifecycle may not emit resumed—fire once after first frame. WidgetsBinding.instance.addPostFrameCallback((_) { // Remove native splash; Flutter login overlay takes over if startup is still running. FlutterNativeSplash.remove(); _reportFacebookActivateApp('first_frame'); }); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } /// With `AutoLogAppEventsEnabled=false`, manually report Facebook install + activateApp. void _reportFacebookActivateApp(String reason) { _fbAppEvents.activateApp().then((_) { if (FacebookConfig.debugLogs) { _fbLog.d('activateApp (manual: $reason)'); } }); } /// Foreground resume; cold start also uses addPostFrameCallback in initState. @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _reportFacebookActivateApp('lifecycle_resumed'); } } @override Widget build(BuildContext context) { return UserCreditsScope( child: TabSelectorScope( selectTab: (tab) => setState(() => _currentTab = tab), child: MaterialApp( title: 'AI Video App', theme: AppTheme.light, debugShowCheckedModeBanner: false, navigatorObservers: [appRouteObserver], initialRoute: '/', builder: (context, child) { return Stack( fit: StackFit.expand, children: [ SafeArea( top: true, left: false, right: false, bottom: false, child: child ?? const SizedBox.shrink(), ), ListenableBuilder( listenable: AuthService.isLoginComplete, builder: (context, _) { if (AuthService.isLoginComplete.value) { return const SizedBox.shrink(); } return const _StartupLoginOverlay(); }, ), ], ); }, routes: { '/': (_) => _MainScaffold( currentTab: _currentTab, onTabSelected: (tab) => setState(() => _currentTab = tab), ), '/recharge': (_) => const RechargeScreen(), '/generate': (ctx) { final task = ModalRoute.of(ctx)?.settings.arguments as TaskItem?; return GenerateVideoScreen(task: task); }, '/progress': (ctx) { final args = ModalRoute.of(ctx)?.settings.arguments; final taskId = args is Map ? args['taskId'] : args; final imagePath = args is Map ? args['imagePath'] as String? : null; return GenerateProgressScreen( taskId: taskId, imagePath: imagePath, ); }, '/result': (ctx) { final mediaItem = ModalRoute.of(ctx)?.settings.arguments as GalleryMediaItem?; return GenerationResultScreen(mediaItem: mediaItem); }, }, ), ), ); } } /// Full-screen wait until [AuthService.loginComplete]; after a delay, show a gentle network hint. class _StartupLoginOverlay extends StatefulWidget { const _StartupLoginOverlay(); @override State<_StartupLoginOverlay> createState() => _StartupLoginOverlayState(); } class _StartupLoginOverlayState extends State<_StartupLoginOverlay> { static const _longWaitAfter = Duration(seconds: 22); Timer? _longWaitTimer; bool _showNetworkHint = false; @override void initState() { super.initState(); _longWaitTimer = Timer(_longWaitAfter, () { if (mounted) setState(() => _showNetworkHint = true); }); } @override void dispose() { _longWaitTimer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Positioned.fill( child: AbsorbPointer( child: Container( color: Colors.black.withValues(alpha: 0.22), child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 28), child: Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(color: AppColors.primary), if (_showNetworkHint) ...[ const SizedBox(height: 22), Text( 'Still getting things ready', textAlign: TextAlign.center, style: TextStyle( color: AppColors.surface.withValues(alpha: 0.96), fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 10), Text( 'This is slower than usual—your connection may be weak. Check Wi‑Fi or mobile data; loading will continue automatically. You can also fully close the app and open it again.', textAlign: TextAlign.center, style: TextStyle( color: AppColors.surface.withValues(alpha: 0.78), fontSize: 14, height: 1.4, ), ), ], ], ), ), ), ), ), ); } } class _MainScaffold extends StatefulWidget { const _MainScaffold({ required this.currentTab, required this.onTabSelected, }); final NavTab currentTab; final ValueChanged onTabSelected; @override State<_MainScaffold> createState() => _MainScaffoldState(); } class _MainScaffoldState extends State<_MainScaffold> with RouteAware { bool _routeObserverSubscribed = false; @override void didChangeDependencies() { super.didChangeDependencies(); if (!_routeObserverSubscribed) { final route = ModalRoute.of(context); if (route is PageRoute) { appRouteObserver.subscribe(this, route); _routeObserverSubscribed = true; } } } @override void dispose() { if (_routeObserverSubscribed) { appRouteObserver.unsubscribe(this); } super.dispose(); } /// Subscribed on `/` [PageRoute]: fires when a pushed route is popped and home is visible again. @override void didPopNext() { if (widget.currentTab == NavTab.home) { homePlaybackResumeNonce.value++; } } @override Widget build(BuildContext context) { return Scaffold( body: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ListenableBuilder( listenable: AuthService.startupUiListenable, builder: (context, _) { if (!AuthService.shouldShowStartupFailure) { return const SizedBox.shrink(); } final msg = AuthService.startupFailureMessage.value ?? 'We couldn’t finish loading. Check your network and tap Retry.'; return Material( color: const Color(0xFFFFF3E0), child: Padding( padding: const EdgeInsets.fromLTRB(16, 10, 12, 10), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.wifi_off_rounded, color: AppColors.primary.withValues(alpha: 0.85), size: 22, ), const SizedBox(width: 12), Expanded( child: Text( msg, style: TextStyle( color: Colors.brown.shade800, fontSize: 14, height: 1.35, ), ), ), TextButton( onPressed: () async { await AuthService.retryStartup(); }, child: const Text('Retry'), ), ], ), ), ); }, ), Expanded( child: IndexedStack( index: widget.currentTab.index, children: [ HomeScreen(isActive: widget.currentTab == NavTab.home), GalleryScreen(isActive: widget.currentTab == NavTab.gallery), ProfileScreen(isActive: widget.currentTab == NavTab.profile), ], ), ), ], ), bottomNavigationBar: BottomNavBar( currentTab: widget.currentTab, onTabSelected: widget.onTabSelected, ), ); } }