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