From be62669d56eeef0637033720d064bddf5679722d Mon Sep 17 00:00:00 2001 From: qguillaume Date: Wed, 6 May 2026 23:05:13 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20int=C3=A9gration=20de=20la=20vue=20vali?= =?UTF-8?q?dation=20image=20(20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/crop/crop_screen.dart | 383 +++++++---------------------- 1 file changed, 83 insertions(+), 300 deletions(-) diff --git a/lib/features/crop/crop_screen.dart b/lib/features/crop/crop_screen.dart index 484a83d8..2a6ff1a0 100644 --- a/lib/features/crop/crop_screen.dart +++ b/lib/features/crop/crop_screen.dart @@ -1,18 +1,9 @@ -/// Écran de recadrage d'image en format carré (1:1). -/// -/// Permet à l'utilisateur de déplacer et zoomer l'image pour sélectionner -/// la zone à recadrer. Le carré de recadrage est fixe au centre de l'écran. -library; - import 'dart:io'; -import 'dart:math' as math; -import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import '../../core/theme/app_theme.dart'; + +// Importations ajustées selon ton arborescence à gauche 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; @@ -29,313 +20,105 @@ class CropScreen extends StatefulWidget { } class _CropScreenState extends State { - final ImageCropService _cropService = ImageCropService(); - - bool _isLoading = false; - bool _imageLoaded = false; - Size? _imageSize; - - // Position et échelle de l'image - Offset _offset = Offset.zero; - double _scale = 1.0; - double _baseScale = 1.0; - Offset _startFocalPoint = Offset.zero; - Offset _startOffset = Offset.zero; - - // Dimensions calculées - double _cropSize = 0; - Size _viewportSize = Size.zero; - - @override - void initState() { - super.initState(); - _loadImageDimensions(); - } - - Future _loadImageDimensions() async { - final file = File(widget.imagePath); - final bytes = await file.readAsBytes(); - final codec = await ui.instantiateImageCodec(bytes); - final frame = await codec.getNextFrame(); - - setState(() { - _imageSize = Size( - frame.image.width.toDouble(), - frame.image.height.toDouble(), - ); - _imageLoaded = true; - }); - } - @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.black, + backgroundColor: const Color(0xFF101214), appBar: AppBar( - backgroundColor: Colors.black, - foregroundColor: Colors.white, - title: const Text('Recadrer'), + backgroundColor: const Color(0xFF101214), + elevation: 0, + centerTitle: true, + title: const Text( + 'Validation image', + style: TextStyle(color: Colors.white, fontSize: 18), + ), leading: IconButton( - icon: const Icon(Icons.close), + icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), - actions: [ - if (!_isLoading) - IconButton( - icon: const Icon(Icons.check), - onPressed: _onCropConfirm, - ), - ], ), - body: _buildBody(), - ); - } - - Widget _buildBody() { - if (!_imageLoaded || _imageSize == null) { - return const Center( - child: CircularProgressIndicator(color: Colors.white), - ); - } - - if (_isLoading) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator(color: Colors.white), - const SizedBox(height: 16), - Text( - 'Recadrage en cours...', - style: TextStyle(color: Colors.white.withValues(alpha: 0.8)), + body: Column( + children: [ + // L'image capturée + 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: Image.file( + File(widget.imagePath), + fit: BoxFit.contain, + ), + ), ), - ], - ), - ); - } + ), - return LayoutBuilder( - builder: (context, constraints) { - _viewportSize = Size(constraints.maxWidth, constraints.maxHeight); + // Bouton Modifier + Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: ElevatedButton.icon( + onPressed: () { + // Logique de modification à venir + }, + icon: const Icon(Icons.crop_rotate), + label: const Text('Modifier'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1A73E8), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ), + ), - // Taille du carré de crop (90% de la plus petite dimension) - _cropSize = - math.min(constraints.maxWidth, constraints.maxHeight) * 0.85; - - // Calculer l'échelle initiale si pas encore fait - if (_scale == 1.0 && _offset == Offset.zero) { - _initializeImagePosition(); - } - - return GestureDetector( - onScaleStart: _onScaleStart, - onScaleUpdate: _onScaleUpdate, - onScaleEnd: _onScaleEnd, - child: Stack( - children: [ - // Image transformée - 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, + // 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('Recommencer', style: TextStyle(color: Colors.white)), ), ), - ), - - // Overlay de recadrage - Positioned.fill( - child: IgnorePointer( - child: CropOverlay(cropSize: _cropSize, showGrid: true), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => AnalysisScreen( + imagePath: widget.imagePath, + targetType: widget.targetType, + ), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1A73E8), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 15), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: const Text('Valider', style: TextStyle(fontWeight: FontWeight.bold)), + ), ), - ), - - // Instructions en bas - Positioned( - left: 0, - right: 0, - bottom: 24, - child: _buildInstructions(), - ), - ], - ), - ); - }, - ); - } - - void _initializeImagePosition() { - if (_imageSize == null) return; - - final imageAspect = _imageSize!.width / _imageSize!.height; - final viewportAspect = _viewportSize.width / _viewportSize.height; - - // Calculer la taille de l'image affichée (avec BoxFit.contain) - double displayWidth, displayHeight; - if (imageAspect > viewportAspect) { - displayWidth = _viewportSize.width; - displayHeight = _viewportSize.width / imageAspect; - } else { - displayHeight = _viewportSize.height; - displayWidth = _viewportSize.height * imageAspect; - } - - // Échelle pour que le plus petit côté de l'image remplisse le carré de crop - final minDisplayDim = math.min(displayWidth, displayHeight); - _scale = _cropSize / minDisplayDim; - - // S'assurer d'un scale minimum - if (_scale < 1.0) _scale = 1.0; - } - - void _onScaleStart(ScaleStartDetails details) { - _baseScale = _scale; - _startFocalPoint = details.focalPoint; - _startOffset = _offset; - } - - void _onScaleUpdate(ScaleUpdateDetails details) { - setState(() { - // Mise à jour du scale - _scale = (_baseScale * details.scale).clamp(0.5, 5.0); - - // Mise à jour de la position - final delta = details.focalPoint - _startFocalPoint; - _offset = _startOffset + delta; - }); - } - - void _onScaleEnd(ScaleEndDetails details) { - // Optionnel: contraindre l'image pour qu'elle couvre toujours le carré de crop - } - - Widget _buildInstructions() { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 24), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.7), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.touch_app, - color: Colors.white.withValues(alpha: 0.8), - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Déplacez et zoomez pour cadrer la cible', - style: TextStyle( - color: Colors.white.withValues(alpha: 0.9), - fontSize: 14, + ], ), ), ], ), ); } - - Future _onCropConfirm() async { - if (_imageSize == null) return; - - setState(() { - _isLoading = true; - }); - - try { - // Calculer la zone de crop en coordonnées normalisées de l'image - final cropRect = _calculateCropRect(); - - // Recadrer l'image - final croppedPath = await _cropService.cropToSquare( - widget.imagePath, - cropRect, - ); - - if (!mounted) return; - - // Naviguer vers l'écran d'analyse avec l'image recadrée - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (_) => AnalysisScreen( - imagePath: croppedPath, - targetType: widget.targetType, - ), - ), - ); - } catch (e) { - if (!mounted) return; - - setState(() { - _isLoading = false; - }); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Erreur lors du recadrage: $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; - - // Calculer la taille de l'image affichée (avec BoxFit.contain) - double displayWidth, displayHeight; - if (imageAspect > viewportAspect) { - displayWidth = _viewportSize.width; - displayHeight = _viewportSize.width / imageAspect; - } else { - displayHeight = _viewportSize.height; - displayWidth = _viewportSize.height * imageAspect; - } - - // Taille de l'image après scale - final scaledWidth = displayWidth * _scale; - final scaledHeight = displayHeight * _scale; - - // Position du centre de l'image dans le viewport - final imageCenterX = _viewportSize.width / 2 + _offset.dx; - final imageCenterY = _viewportSize.height / 2 + _offset.dy; - - // Position du coin supérieur gauche de l'image - final imageLeft = imageCenterX - scaledWidth / 2; - final imageTop = imageCenterY - scaledHeight / 2; - - // Position du carré de crop (centré dans le viewport) - final cropLeft = (_viewportSize.width - _cropSize) / 2; - final cropTop = (_viewportSize.height - _cropSize) / 2; - - // Convertir en coordonnées relatives à l'image affichée - final relCropLeft = (cropLeft - imageLeft) / scaledWidth; - final relCropTop = (cropTop - imageTop) / scaledHeight; - final relCropSize = _cropSize / scaledWidth; - final relCropSizeY = _cropSize / scaledHeight; - - return CropRect( - x: relCropLeft.clamp(0.0, 1.0), - y: relCropTop.clamp(0.0, 1.0), - width: relCropSize.clamp(0.0, 1.0 - relCropLeft.clamp(0.0, 1.0)), - height: relCropSizeY.clamp(0.0, 1.0 - relCropTop.clamp(0.0, 1.0)), - ); - } -} +} \ No newline at end of file