import 'package:flutter/material.dart'; import 'config/strings.dart'; import 'config/title_server_config.dart'; import 'services/api_service.dart'; import 'services/file_picker_service.dart'; import 'services/qr_service.dart'; import 'pages/home_page.dart'; import 'pages/settings_page.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await TitleServerConfigHolder().init(); runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: AppStrings.appTitle, debugShowCheckedModeBanner: false, themeMode: ThemeMode.system, theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: Colors.blue, brightness: Brightness.light, ), ), darkTheme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: Colors.blue, brightness: Brightness.dark, ), ), home: const LoginPage(), ); } } class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State createState() => _LoginPageState(); } class _LoginPageState extends State { final _qrController = TextEditingController(); final _apiService = ApiService(); String _qrCode = ''; bool _loading = false; @override void dispose() { _qrController.dispose(); super.dispose(); } void _onQRContentChanged(String value) { setState(() { _qrCode = _apiService.extractQRCode(value.trim()); }); } Future _onUploadQR() async { try { final bytes = await pickImageBytes(); if (bytes == null) return; final result = await decodeQRFromBytes(bytes); if (result != null && result.isNotEmpty) { _qrController.text = result; setState(() { _qrCode = _apiService.extractQRCode(result); }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text(AppStrings.qrScanSuccess)), ); } } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text(AppStrings.qrScanFailed)), ); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${AppStrings.qrScanError}: $e')), ); } } } Future _onLogin() async { if (_qrCode.isEmpty) return; setState(() => _loading = true); try { final result = await _apiService.login(_qrCode); if (!mounted) return; if (result.success) { if (!mounted) return; Navigator.of(context).push( MaterialPageRoute( builder: (_) => HomePage( userId: result.userId, token: result.token, ), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${AppStrings.loginFailed} (errorID: ${result.errorId})')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${AppStrings.requestFailed}: $e')), ); } } finally { if (mounted) { setState(() => _loading = false); } } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text(AppStrings.appTitle), actions: [ IconButton( icon: const Icon(Icons.settings), tooltip: AppStrings.settingsTooltip, onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SettingsPage()), ); }, ), ], ), body: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 440), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.qr_code_scanner, size: 64, color: theme.colorScheme.primary, ), const SizedBox(height: 16), Text( AppStrings.appTitle, style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), ), const SizedBox(height: 8), Text( AppStrings.loginSubtitle, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 40), Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( color: theme.colorScheme.outline.withValues(alpha: 0.3), ), ), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.key, size: 18, color: theme.colorScheme.primary, ), const SizedBox(width: 8), Text( AppStrings.qrCodeToken, style: theme.textTheme.labelLarge?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 12), TextField( controller: _qrController, maxLines: 3, onChanged: _onQRContentChanged, style: theme.textTheme.bodyMedium?.copyWith( fontFamily: 'monospace', letterSpacing: 0.5, ), decoration: InputDecoration( hintText: AppStrings.qrHint, hintStyle: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant .withValues(alpha: 0.5), fontFamily: 'monospace', ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), ), contentPadding: const EdgeInsets.all(14), ), ), ], ), ), ), if (_qrCode.isNotEmpty) ...[ const SizedBox(height: 12), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest .withValues(alpha: 0.5), borderRadius: BorderRadius.circular(10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppStrings.qrParsedResult, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 6), Text( _qrCode, style: theme.textTheme.bodySmall?.copyWith( fontFamily: 'monospace', color: theme.colorScheme.onSurface, ), ), ], ), ), ], const SizedBox(height: 28), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: _loading ? null : _onUploadQR, icon: const Icon(Icons.image, size: 20), label: const Text(AppStrings.uploadQR), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), const SizedBox(width: 16), Expanded( child: FilledButton.icon( onPressed: (_qrCode.isNotEmpty && !_loading) ? _onLogin : null, icon: _loading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.arrow_forward, size: 20), label: Text(_loading ? AppStrings.loggingIn : AppStrings.login), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ), ], ), ), ), ), ); } }