import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'login.dart'; import 'screens/improved_home_screen.dart'; import 'services/auth_service.dart'; import 'services/server_discovery_service.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, ), ); // ✅ Load persisted auth session (token/user) from secure storage final authService = AuthService(); await authService.loadToken(); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); static const _primary = Color(0xFF1565C0); static const _accent = Color(0xFF00897B); static const _surfaceTint = Color(0xFFF3F7FB); @override Widget build(BuildContext context) { return MaterialApp( title: 'Kontrak Kampus', theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: _primary, primary: _primary, secondary: _accent, surface: Colors.white, brightness: Brightness.light, ), useMaterial3: true, scaffoldBackgroundColor: _surfaceTint, canvasColor: _surfaceTint, splashFactory: InkSparkle.splashFactory, textTheme: const TextTheme( titleLarge: TextStyle( fontWeight: FontWeight.w700, letterSpacing: 0.2, color: Color(0xFF1B2A41), ), titleMedium: TextStyle( fontWeight: FontWeight.w600, letterSpacing: 0.15, color: Color(0xFF23324D), ), bodyLarge: TextStyle( fontWeight: FontWeight.w500, color: Color(0xFF2B3A55), ), bodyMedium: TextStyle(color: Color(0xFF3B4A64)), ), appBarTheme: const AppBarTheme( backgroundColor: _primary, foregroundColor: Colors.white, elevation: 0, centerTitle: true, titleTextStyle: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white, ), ), cardTheme: CardThemeData( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), color: Colors.white, shadowColor: Colors.black.withOpacity(0.05), margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( elevation: 0, backgroundColor: _primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), textStyle: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, letterSpacing: 0.3, ), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: _primary, side: const BorderSide(color: Color(0xFFD2DFEF)), padding: const EdgeInsets.symmetric(vertical: 13, horizontal: 18), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), textStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, letterSpacing: 0.2, ), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: _primary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), textStyle: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), ), ), progressIndicatorTheme: const ProgressIndicatorThemeData( color: _primary, linearTrackColor: Color(0xFFE2EAF3), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: const Color(0xFFFAFCFF), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFFE2EAF3)), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFFE2EAF3)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _primary, width: 1.5), ), hintStyle: const TextStyle( color: Color(0xFF8D9BB0), fontWeight: FontWeight.w500, ), ), chipTheme: ChipThemeData( backgroundColor: const Color(0xFFE9F1FB), selectedColor: const Color(0xFF1565C0), secondarySelectedColor: const Color(0xFF1565C0), disabledColor: const Color(0xFFE2E8F0), labelStyle: const TextStyle( color: Color(0xFF34435E), fontWeight: FontWeight.w600, ), secondaryLabelStyle: const TextStyle( color: Colors.white, fontWeight: FontWeight.w700, ), side: const BorderSide(color: Color(0xFFD6E2F1)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, backgroundColor: const Color(0xFF1C2A44), contentTextStyle: const TextStyle( color: Colors.white, fontWeight: FontWeight.w500, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), dialogTheme: DialogThemeData( backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), bottomSheetTheme: const BottomSheetThemeData( backgroundColor: Colors.white, modalBackgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), ), bottomNavigationBarTheme: const BottomNavigationBarThemeData( backgroundColor: Colors.white, selectedItemColor: _primary, unselectedItemColor: Color(0xFF94A0B2), type: BottomNavigationBarType.fixed, elevation: 8, ), navigationBarTheme: NavigationBarThemeData( backgroundColor: Colors.white, indicatorColor: _primary.withOpacity(0.12), labelTextStyle: WidgetStateProperty.resolveWith((states) { final selected = states.contains(WidgetState.selected); return TextStyle( fontSize: 12, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, color: selected ? _primary : const Color(0xFF94A0B2), ); }), iconTheme: WidgetStateProperty.resolveWith((states) { final selected = states.contains(WidgetState.selected); return IconThemeData( color: selected ? _primary : const Color(0xFF94A0B2), size: 24, ); }), ), listTileTheme: const ListTileThemeData( iconColor: Color(0xFF5A6B85), textColor: Color(0xFF24344D), tileColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), ), dividerTheme: const DividerThemeData( color: Color(0xFFE8EDF4), thickness: 1, space: 0, ), pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.android: FadeForwardsPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(), TargetPlatform.macOS: FadeUpwardsPageTransitionsBuilder(), TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(), }, ), ), home: const SplashScreen(), debugShowCheckedModeBanner: false, ); } } // Splash screen dengan animasi class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @override State createState() => _SplashScreenState(); } class _SplashScreenState extends State with SingleTickerProviderStateMixin { final _authService = AuthService(); late AnimationController _animController; late Animation _fadeAnim; late Animation _scaleAnim; String _statusText = 'Memulai aplikasi...'; @override void initState() { super.initState(); _animController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), ); _fadeAnim = Tween( begin: 0, end: 1, ).animate(CurvedAnimation(parent: _animController, curve: Curves.easeOut)); _scaleAnim = Tween(begin: 0.7, end: 1).animate( CurvedAnimation(parent: _animController, curve: Curves.elasticOut), ); _animController.forward(); _checkAuth(); } @override void dispose() { _animController.dispose(); super.dispose(); } Future _checkAuth() async { // 🔍 Auto-detect server IP dengan fallback ke konfigurasi default. // Ini membantu APK tetap bisa konek saat IP LAN berubah. await ServerDiscoveryService.discover( onStatus: (status) { if (mounted) setState(() => _statusText = status); }, ).timeout( const Duration(seconds: 8), onTimeout: () { debugPrint('Server discovery timeout, using configured server URL'); return false; }, ); if (mounted) setState(() => _statusText = 'Memeriksa sesi...'); await _authService.loadToken(); await Future.delayed(const Duration(milliseconds: 400)); if (!mounted) return; final destination = _authService.isAuthenticated ? const ImprovedHomeScreen() : const LoginScreen(); Navigator.pushReplacement( context, PageRouteBuilder( pageBuilder: (_, __, ___) => destination, transitionsBuilder: (_, animation, __, child) { return FadeTransition(opacity: animation, child: child); }, transitionDuration: const Duration(milliseconds: 500), ), ); } @override Widget build(BuildContext context) { return Scaffold( body: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF1565C0), Color(0xFF0D47A1), Color(0xFF1A237E)], ), ), child: Center( child: FadeTransition( opacity: _fadeAnim, child: ScaleTransition( scale: _scaleAnim, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(28), decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 30, offset: const Offset(0, 10), ), ], ), child: const Icon( Icons.school_rounded, size: 64, color: Color(0xFF1565C0), ), ), const SizedBox(height: 32), const Text( 'Kontrak Kampus', style: TextStyle( fontSize: 30, fontWeight: FontWeight.w800, color: Colors.white, letterSpacing: 0.5, ), ), const SizedBox(height: 8), Text( 'Rekomendasi Kontrakan & Laundry untuk Mahasiswa', style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.8), letterSpacing: 0.3, ), ), const SizedBox(height: 48), SizedBox( width: 32, height: 32, child: CircularProgressIndicator( strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation( Colors.white.withOpacity(0.9), ), ), ), const SizedBox(height: 16), Text( _statusText, style: TextStyle( fontSize: 13, color: Colors.white.withOpacity(0.7), ), ), ], ), ), ), ), ), ); } }