at main 4.6 kB view raw
1import 'package:flutter/foundation.dart'; 2import 'package:flutter/material.dart'; 3import 'package:flutter_dotenv/flutter_dotenv.dart'; 4import 'package:flutter_riverpod/flutter_riverpod.dart'; 5import 'package:grain/api.dart'; 6import 'package:grain/app_logger.dart'; 7import 'package:grain/app_theme.dart'; 8import 'package:grain/auth.dart'; 9import 'package:grain/providers/notifications_provider.dart'; 10import 'package:grain/providers/profile_provider.dart'; 11import 'package:grain/screens/home_page.dart'; 12import 'package:grain/screens/login_page.dart'; 13import 'package:grain/websocket_service.dart'; 14 15import 'widgets/skeleton_timeline.dart'; 16 17class AppConfig { 18 static late final String apiUrl; 19 static late final String wsUrl; 20 21 static Future<void> init() async { 22 if (!kReleaseMode) { 23 await dotenv.load(fileName: '.env'); 24 } 25 apiUrl = kReleaseMode 26 ? const String.fromEnvironment('API_URL', defaultValue: 'https://grain.social') 27 : dotenv.env['API_URL'] ?? ''; 28 wsUrl = kReleaseMode 29 ? const String.fromEnvironment( 30 'WS_URL', 31 defaultValue: 'wss://notifications.grainsocial.network/ws', 32 ) 33 : dotenv.env['WS_URL'] ?? ''; 34 } 35} 36 37Future<void> main() async { 38 WidgetsFlutterBinding.ensureInitialized(); 39 FlutterError.onError = (FlutterErrorDetails details) { 40 FlutterError.presentError(details); 41 appLogger.e('Flutter error: ${details.exception}\n${details.stack}'); 42 }; 43 await AppConfig.init(); 44 appLogger.i('🚀 App started'); 45 runApp(const ProviderScope(child: MyApp())); 46} 47 48class MyApp extends StatefulWidget { 49 const MyApp({super.key}); 50 51 @override 52 State<MyApp> createState() => _MyAppState(); 53} 54 55class _MyAppState extends State<MyApp> with WidgetsBindingObserver { 56 bool isSignedIn = false; 57 bool _loading = true; 58 WebSocketService? _wsService; 59 60 @override 61 void initState() { 62 super.initState(); 63 WidgetsBinding.instance.addObserver(this); 64 _checkToken(); 65 } 66 67 @override 68 void dispose() { 69 WidgetsBinding.instance.removeObserver(this); 70 _disconnectWebSocket(); 71 super.dispose(); 72 } 73 74 Future<void> _connectWebSocket() async { 75 if (_wsService != null) return; // Already connected 76 _disconnectWebSocket(); 77 if (!isSignedIn) return; 78 final session = await auth.getValidSession(); 79 if (session == null) return; 80 _wsService = WebSocketService( 81 wsUrl: AppConfig.wsUrl, 82 accessToken: session.token, 83 onMessage: (message) { 84 // Optionally: handle global messages or trigger provider updates 85 }, 86 ); 87 _wsService!.connect(); 88 } 89 90 void _disconnectWebSocket() { 91 _wsService?.disconnect(); 92 _wsService = null; 93 } 94 95 @override 96 void didChangeAppLifecycleState(AppLifecycleState state) { 97 if (state == AppLifecycleState.resumed && isSignedIn) { 98 // ignore: unawaited_futures 99 _connectWebSocket(); 100 } else if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { 101 _disconnectWebSocket(); 102 } 103 } 104 105 Future<void> _checkToken() async { 106 final user = await apiService.fetchCurrentUser(); 107 final valid = user != null; 108 setState(() { 109 isSignedIn = valid; 110 _loading = false; 111 }); 112 } 113 114 // Invalidate providers to refresh data 115 void _invalidateProviders() { 116 final container = ProviderScope.containerOf(context, listen: false); 117 container.invalidate(profileNotifierProvider); 118 container.invalidate(notificationsProvider); 119 } 120 121 void _handleSignIn() async { 122 setState(() { 123 isSignedIn = true; 124 }); 125 appLogger.i('Fetching current user after sign in'); 126 await apiService.fetchCurrentUser(); 127 await _connectWebSocket(); 128 _invalidateProviders(); 129 } 130 131 void _handleSignOut(BuildContext context) async { 132 await auth.logout(); 133 setState(() { 134 isSignedIn = false; 135 }); 136 _disconnectWebSocket(); 137 } 138 139 @override 140 Widget build(BuildContext context) { 141 Widget home; 142 if (_loading) { 143 home = Scaffold( 144 appBar: AppBar(title: const Text('Grain')), 145 body: Column( 146 children: [ 147 Expanded( 148 child: SkeletonTimeline(padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8)), 149 ), 150 ], 151 ), 152 ); 153 } else { 154 home = isSignedIn 155 ? MyHomePage(title: 'Grain', onSignOut: () => _handleSignOut(context)) 156 : LoginPage(onSignIn: _handleSignIn); 157 } 158 return MaterialApp( 159 title: 'Grain', 160 theme: AppTheme.lightTheme, 161 darkTheme: AppTheme.darkTheme, 162 themeMode: ThemeMode.system, 163 home: home, 164 ); 165 } 166}