import 'dart:io'; import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../core/theme/app_theme.dart'; import '../../data/models/target_type.dart'; import '../../services/image_crop_service.dart'; import '../analysis/analysis_screen.dart'; import 'widgets/crop_overlay.dart'; class CropScreen extends StatefulWidget { final String imagePath; final TargetType targetType; final double? initialScale; final Offset? initialOffset; const CropScreen({ super.key, required this.imagePath, required this.targetType, this.initialScale, this.initialOffset, }); @override State createState() => _CropScreenState(); } class _CropScreenState extends State { final ImageCropService _cropService = ImageCropService(); // États de transformation double _scale = 1.0; double _baseScale = 1.0; Offset _offset = Offset.zero; Offset _startOffset = Offset.zero; Offset _startFocalPoint = Offset.zero; bool _isLoading = false; bool _imageLoaded = false; Size? _imageSize; late Size _viewportSize; late double _cropSize; @override void initState() { super.initState(); _loadImageInfo(); } Future _loadImageInfo() async { final file = File(widget.imagePath); final decodedImage = await decodeImageFromList(await file.readAsBytes()); if (mounted) { setState(() { _imageSize = Size( decodedImage.width.toDouble(), decodedImage.height.toDouble(), ); _imageLoaded = true; }); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF101214), appBar: AppBar( backgroundColor: const Color(0xFF101214), elevation: 0, centerTitle: true, title: const Text( 'Centrage de la cible', style: TextStyle(color: Colors.white, fontSize: 18), ), leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), ), body: _isLoading ? const Center(child: CircularProgressIndicator(color: Color(0xFF1A73E8))) : Column( children: [ // Zone interactive de crop Expanded( child: Container( margin: const EdgeInsets.all(20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white10), ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: _imageLoaded ? _buildInteractiveCrop() : const Center(child: CircularProgressIndicator()), ), ), ), // Bouton d'aide / Modifier Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // CORRECTION : Icône passée en vert fluo (0xFF00FF00) const Icon(Icons.center_focus_strong, color: Color(0xFF00FF00), size: 20), const SizedBox(width: 8), Text( 'Alignez le centre de la cible sur la croix', style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.w500), ), ], ), const SizedBox(height: 4), Text( 'Zoomez pour plus de précision', style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12), ), ], ), ), // Boutons du bas Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 30), child: Row( children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.white24), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: const Text('Retour', style: TextStyle(color: Colors.white)), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton( onPressed: _onCropConfirm, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF1A73E8), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), // CORRECTION : Texte modifié pour correspondre au nouveau flux child: const Text('Suivant', style: TextStyle(fontWeight: FontWeight.bold)), ), ), ], ), ), ], ), ); } Widget _buildInteractiveCrop() { return LayoutBuilder( builder: (context, constraints) { _viewportSize = Size(constraints.maxWidth, constraints.maxHeight); // Le carré de crop occupe presque tout l'espace disponible _cropSize = math.min(constraints.maxWidth, constraints.maxHeight) * 0.95; if (_scale == 1.0 && _offset == Offset.zero) { _initializeImagePosition(); } return GestureDetector( onScaleStart: _onScaleStart, onScaleUpdate: _onScaleUpdate, child: Stack( children: [ Positioned.fill( child: Center( child: Transform( transform: Matrix4.identity() ..setTranslationRaw(_offset.dx, _offset.dy, 0) ..scale(_scale, _scale), alignment: Alignment.center, child: Image.file( File(widget.imagePath), fit: BoxFit.contain, width: _viewportSize.width, height: _viewportSize.height, ), ), ), ), Positioned.fill( child: IgnorePointer( child: CropOverlay(cropSize: _cropSize, showGrid: false), ), ), ], ), ); }, ); } void _initializeImagePosition() { if (_imageSize == null) return; final imageAspect = _imageSize!.width / _imageSize!.height; final viewportAspect = _viewportSize.width / _viewportSize.height; double displayWidth, displayHeight; if (imageAspect > viewportAspect) { displayWidth = _viewportSize.width; displayHeight = _viewportSize.width / imageAspect; } else { displayHeight = _viewportSize.height; displayWidth = _viewportSize.height * imageAspect; } final minDisplayDim = math.min(displayWidth, displayHeight); _scale = widget.initialScale ?? (_cropSize / minDisplayDim); if (_scale < 1.0 && widget.initialScale == null) _scale = 1.0; if (widget.initialOffset != null) { _offset = widget.initialOffset!; } } void _onScaleStart(ScaleStartDetails details) { _baseScale = _scale; _startFocalPoint = details.focalPoint; _startOffset = _offset; } void _onScaleUpdate(ScaleUpdateDetails details) { setState(() { _scale = (_baseScale * details.scale).clamp(0.8, 8.0); final delta = details.focalPoint - _startFocalPoint; _offset = _startOffset + delta; }); } Future _onCropConfirm() async { setState(() => _isLoading = true); try { final cropRect = _calculateCropRect(); // On calcule le centre relatif basé sur le centrage utilisateur final targetCenterX = cropRect.x + cropRect.width / 2; final targetCenterY = cropRect.y + cropRect.height / 2; if (!mounted) return; // Note : On laisse temporairement AnalysisScreen ici. // Dès qu'on s'attaque au fichier de calibration, on modifiera cette ligne // pour basculer directement sur le bon écran ! Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => AnalysisScreen( imagePath: widget.imagePath, targetType: widget.targetType, targetCenter: Offset(targetCenterX, targetCenterY), cropScale: _scale, cropOffset: _offset, ), ), ); } catch (e) { if (mounted) { setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor), ); } } } CropRect _calculateCropRect() { if (_imageSize == null) return const CropRect(x: 0, y: 0, width: 1, height: 1); final imageAspect = _imageSize!.width / _imageSize!.height; final viewportAspect = _viewportSize.width / _viewportSize.height; double displayWidth, displayHeight; if (imageAspect > viewportAspect) { displayWidth = _viewportSize.width; displayHeight = _viewportSize.width / imageAspect; } else { displayHeight = _viewportSize.height; displayWidth = _viewportSize.height * imageAspect; } final scaledWidth = displayWidth * _scale; final scaledHeight = displayHeight * _scale; final imageCenterX = _viewportSize.width / 2 + _offset.dx; final imageCenterY = _viewportSize.height / 2 + _offset.dy; final imageLeft = imageCenterX - scaledWidth / 2; final imageTop = imageCenterY - scaledHeight / 2; final cropLeft = (_viewportSize.width - _cropSize) / 2; final cropTop = (_viewportSize.height - _cropSize) / 2; final relCropLeft = (cropLeft - imageLeft) / scaledWidth; final relCropTop = (cropTop - imageTop) / scaledHeight; final relCropWidth = _cropSize / scaledWidth; final relCropHeight = _cropSize / scaledHeight; return CropRect( x: relCropLeft, y: relCropTop, width: relCropWidth, height: relCropHeight, ); } }