import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:camera/camera.dart'; import '../bloc/scanner/scanner_bloc.dart'; import '../bloc/scanner/scanner_event.dart'; import '../bloc/scanner/scanner_state.dart'; import '../services/camera_service.dart'; class ScannerScreen extends StatelessWidget { final String? geminiApiKey; final String? openaiApiKey; final String openaiBaseUrl; const ScannerScreen({ super.key, this.geminiApiKey, this.openaiApiKey, this.openaiBaseUrl = 'http://localhost:8317', }); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => ScannerBloc(cameraService: CameraService())..add(InitializeCamera()), child: _ScannerScreenContent( geminiApiKey: geminiApiKey, openaiApiKey: openaiApiKey, openaiBaseUrl: openaiBaseUrl, ), ); } } class _ScannerScreenContent extends StatelessWidget { final String? geminiApiKey; final String? openaiApiKey; final String openaiBaseUrl; const _ScannerScreenContent({ this.geminiApiKey, this.openaiApiKey, this.openaiBaseUrl = 'http://localhost:8317', }); void _showErrorDialog(BuildContext context, String errorMessage) { showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Ошибка'), content: Text(errorMessage), actions: [ TextButton( onPressed: () { Navigator.of(dialogContext).pop(); context.read().add(DismissError()); }, child: const Text('OK'), ), ], ), ); } @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { // Show error dialog when error message is present if (state.errorMessage != null) { _showErrorDialog(context, state.errorMessage!); } // Navigate back with analyzed book if (state.analyzedBook != null) { Navigator.pop(context, state.analyzedBook); } }, child: BlocBuilder( builder: (context, state) { final cameraService = context.read().cameraService; return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ // Camera preview if (state.isInitialized && cameraService.controller != null) Center(child: CameraPreview(cameraService.controller!)) else if (state.hasPermissionError) _buildPermissionError(context) else _buildLoading(), // Scan frame overlay if (state.isInitialized && !state.isAnalyzing) _buildScanFrame(), // Header _buildHeader(context), // Processing overlay if (state.isAnalyzing) _buildProcessingOverlay(), // Controls if (state.isInitialized && !state.isAnalyzing) _buildControls(context, state.isCapturing), // Instructions if (state.isInitialized && !state.isAnalyzing) _buildInstructions(), ], ), ); }, ), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.white), ), ); } Widget _buildPermissionError(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.camera_alt_outlined, size: 64, color: Colors.white38, ), const SizedBox(height: 16), const Text( 'Нет доступа к камере', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), const Text( 'Разрешите доступ к камере для сканирования обложек книг', style: TextStyle(color: Colors.white70, fontSize: 14), textAlign: TextAlign.center, ), const SizedBox(height: 24), ElevatedButton( onPressed: () { context.read().add(InitializeCamera()); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF17CF54), ), child: const Text('Повторить'), ), ], ), ), ); } Widget _buildScanFrame() { return Center( child: FractionallySizedBox( widthFactor: 0.75, child: AspectRatio( aspectRatio: 2 / 3, child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.white30, width: 2), borderRadius: BorderRadius.circular(12), ), // Corner accents child: Stack( children: [ // Top left corner Positioned( top: -2, left: -2, child: Container( width: 20, height: 20, decoration: const BoxDecoration( border: Border( left: BorderSide(color: Color(0xFF17CF54), width: 4), top: BorderSide(color: Color(0xFF17CF54), width: 4), ), ), ), ), // Top right corner Positioned( top: -2, right: -2, child: Container( width: 20, height: 20, decoration: const BoxDecoration( border: Border( right: BorderSide(color: Color(0xFF17CF54), width: 4), top: BorderSide(color: Color(0xFF17CF54), width: 4), ), ), ), ), // Bottom left corner Positioned( bottom: -2, left: -2, child: Container( width: 20, height: 20, decoration: const BoxDecoration( border: Border( left: BorderSide(color: Color(0xFF17CF54), width: 4), bottom: BorderSide(color: Color(0xFF17CF54), width: 4), ), ), ), ), // Bottom right corner Positioned( bottom: -2, right: -2, child: Container( width: 20, height: 20, decoration: const BoxDecoration( border: Border( right: BorderSide(color: Color(0xFF17CF54), width: 4), bottom: BorderSide(color: Color(0xFF17CF54), width: 4), ), ), ), ), ], ), ), ), ), ); } Widget _buildHeader(BuildContext context) { return SafeArea( child: Padding( padding: const EdgeInsets.all(8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.pop(context), ), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: const Color(0xFF17CF54), borderRadius: BorderRadius.circular(8), ), child: const Text( 'СКАНЕР', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, color: Colors.white, ), ), ), const SizedBox(width: 48), ], ), ), ); } Widget _buildProcessingOverlay() { return Container( color: Colors.black87, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFF17CF54)), ), const SizedBox(height: 24), const Text( 'Анализ обложки...', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text( 'Это может занять несколько секунд', style: TextStyle(color: Colors.grey.shade400, fontSize: 14), ), ], ), ), ); } Widget _buildControls(BuildContext context, bool isCapturing) { return Positioned( bottom: 50, left: 0, right: 0, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Flash button (placeholder) Container( width: 50, height: 50, margin: const EdgeInsets.only(right: 20), child: IconButton( icon: const Icon(Icons.flash_off, color: Colors.white), onPressed: () { // Flash functionality can be added later }, ), ), // Capture button GestureDetector( onTap: isCapturing ? null : () { context.read().add( CaptureAndAnalyze( openaiApiKey: openaiApiKey, openaiBaseUrl: openaiBaseUrl, geminiApiKey: geminiApiKey, ), ); }, child: Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 4), ), child: Center( child: Container( width: 64, height: 64, decoration: BoxDecoration( shape: BoxShape.circle, color: isCapturing ? Colors.white38 : Colors.white, ), ), ), ), ), // Camera switch button Container( width: 50, height: 50, margin: const EdgeInsets.only(left: 20), child: IconButton( icon: const Icon(Icons.flip_camera_ios, color: Colors.white), onPressed: () { context.read().add(SwitchCamera()); }, ), ), ], ), ); } Widget _buildInstructions() { return Positioned( bottom: 140, left: 0, right: 0, child: Column( children: [ Text( 'Поместите обложку в рамку', textAlign: TextAlign.center, style: TextStyle( color: Colors.grey.shade400, fontSize: 16, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), Text( 'Убедитесь, что текст читается четко', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade500, fontSize: 12), ), ], ), ); } }