1import 'package:flutter/foundation.dart'; 2import 'package:flutter/material.dart'; 3import 'package:flutter/services.dart'; 4import 'package:go_router/go_router.dart'; 5import 'package:provider/provider.dart'; 6 7import 'config/oauth_config.dart'; 8import 'models/post.dart'; 9import 'providers/auth_provider.dart'; 10import 'providers/comments_provider.dart'; 11import 'providers/feed_provider.dart'; 12import 'providers/vote_provider.dart'; 13import 'screens/auth/login_screen.dart'; 14import 'screens/home/main_shell_screen.dart'; 15import 'screens/home/post_detail_screen.dart'; 16import 'screens/landing_screen.dart'; 17import 'services/streamable_service.dart'; 18import 'services/vote_service.dart'; 19import 'widgets/loading_error_states.dart'; 20 21void main() async { 22 WidgetsFlutterBinding.ensureInitialized(); 23 24 // Set system UI overlay style (Android navigation bar) 25 SystemChrome.setSystemUIOverlayStyle( 26 const SystemUiOverlayStyle( 27 systemNavigationBarColor: Color(0xFF0B0F14), 28 systemNavigationBarIconBrightness: Brightness.light, 29 ), 30 ); 31 32 // Initialize auth provider 33 final authProvider = AuthProvider(); 34 await authProvider.initialize(); 35 36 // Initialize vote service with auth callbacks for direct PDS writes 37 // Uses DPoP authentication (not Bearer tokens!) 38 final voteService = VoteService( 39 sessionGetter: () async => authProvider.session, 40 didGetter: () => authProvider.did, 41 pdsUrlGetter: authProvider.getPdsUrl, 42 ); 43 44 runApp( 45 MultiProvider( 46 providers: [ 47 ChangeNotifierProvider.value(value: authProvider), 48 ChangeNotifierProvider( 49 create: 50 (_) => VoteProvider( 51 voteService: voteService, 52 authProvider: authProvider, 53 ), 54 ), 55 ChangeNotifierProxyProvider2<AuthProvider, VoteProvider, FeedProvider>( 56 create: 57 (context) => FeedProvider( 58 authProvider, 59 voteProvider: context.read<VoteProvider>(), 60 voteService: voteService, 61 ), 62 update: (context, auth, vote, previous) { 63 // Reuse existing provider to maintain state across rebuilds 64 return previous ?? 65 FeedProvider( 66 auth, 67 voteProvider: vote, 68 voteService: voteService, 69 ); 70 }, 71 ), 72 ChangeNotifierProxyProvider2<AuthProvider, VoteProvider, 73 CommentsProvider>( 74 create: 75 (context) => CommentsProvider( 76 authProvider, 77 voteProvider: context.read<VoteProvider>(), 78 voteService: voteService, 79 ), 80 update: (context, auth, vote, previous) { 81 // Reuse existing provider to maintain state across rebuilds 82 return previous ?? 83 CommentsProvider( 84 auth, 85 voteProvider: vote, 86 voteService: voteService, 87 ); 88 }, 89 ), 90 // StreamableService for video embeds 91 Provider<StreamableService>(create: (_) => StreamableService()), 92 ], 93 child: const CovesApp(), 94 ), 95 ); 96} 97 98class CovesApp extends StatelessWidget { 99 const CovesApp({super.key}); 100 101 @override 102 Widget build(BuildContext context) { 103 final authProvider = Provider.of<AuthProvider>(context, listen: false); 104 105 return MaterialApp.router( 106 title: 'Coves', 107 theme: ThemeData( 108 colorScheme: ColorScheme.fromSeed( 109 seedColor: const Color(0xFFFF6B35), 110 brightness: Brightness.dark, 111 ), 112 useMaterial3: true, 113 ), 114 routerConfig: _createRouter(authProvider), 115 restorationScopeId: 'app', 116 debugShowCheckedModeBanner: false, 117 ); 118 } 119} 120 121// GoRouter configuration factory 122GoRouter _createRouter(AuthProvider authProvider) { 123 return GoRouter( 124 routes: [ 125 GoRoute(path: '/', builder: (context, state) => const LandingScreen()), 126 GoRoute(path: '/login', builder: (context, state) => const LoginScreen()), 127 GoRoute( 128 path: '/feed', 129 builder: (context, state) => const MainShellScreen(), 130 ), 131 GoRoute( 132 path: '/post/:postUri', 133 builder: (context, state) { 134 // Extract post from state.extra 135 final post = state.extra as FeedViewPost?; 136 137 // If no post provided via extra, show user-friendly error 138 if (post == null) { 139 if (kDebugMode) { 140 print('⚠️ PostDetailScreen: No post provided in route extras'); 141 } 142 // Show not found screen with option to go back 143 return NotFoundError( 144 title: 'Post Not Found', 145 message: 146 'This post could not be loaded. It may have been deleted or the link is invalid.', 147 onBackPressed: () { 148 // Navigate back to feed 149 context.go('/feed'); 150 }, 151 ); 152 } 153 154 return PostDetailScreen(post: post); 155 }, 156 ), 157 ], 158 refreshListenable: authProvider, 159 redirect: (context, state) { 160 final isAuthenticated = authProvider.isAuthenticated; 161 final isLoading = authProvider.isLoading; 162 final currentPath = state.uri.path; 163 164 // Don't redirect while loading initial auth state 165 if (isLoading) { 166 return null; 167 } 168 169 // If authenticated and on landing/login screen, redirect to feed 170 if (isAuthenticated && (currentPath == '/' || currentPath == '/login')) { 171 if (kDebugMode) { 172 print('🔄 User authenticated, redirecting to /feed'); 173 } 174 return '/feed'; 175 } 176 177 // Allow anonymous users to access /feed for browsing 178 // Sign-out redirect is handled explicitly in the sign-out action 179 return null; 180 }, 181 errorBuilder: (context, state) { 182 // Check if this is an OAuth callback 183 if (state.uri.scheme == OAuthConfig.customScheme) { 184 if (kDebugMode) { 185 print( 186 '⚠️ OAuth callback in errorBuilder - ' 187 'flutter_web_auth_2 should handle it', 188 ); 189 print(' URI: ${state.uri}'); 190 } 191 // Return nothing - just stay on current screen 192 // flutter_web_auth_2 will process the callback at native level 193 return const SizedBox.shrink(); 194 } 195 196 // For other errors, show landing page 197 if (kDebugMode) { 198 print('⚠️ Router error: ${state.uri}'); 199 } 200 return const LandingScreen(); 201 }, 202 ); 203}