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