feat: intégration de la vue validation image (20)

This commit is contained in:
qguillaume
2026-05-06 23:05:13 +02:00
parent db6dd0cee5
commit be62669d56

View File

@@ -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:io';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; 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 '../../data/models/target_type.dart';
import '../../services/image_crop_service.dart';
import '../analysis/analysis_screen.dart'; import '../analysis/analysis_screen.dart';
import 'widgets/crop_overlay.dart';
class CropScreen extends StatefulWidget { class CropScreen extends StatefulWidget {
final String imagePath; final String imagePath;
@@ -29,313 +20,105 @@ class CropScreen extends StatefulWidget {
} }
class _CropScreenState extends State<CropScreen> { class _CropScreenState extends State<CropScreen> {
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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: const Color(0xFF101214),
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.black, backgroundColor: const Color(0xFF101214),
foregroundColor: Colors.white, elevation: 0,
title: const Text('Recadrer'), centerTitle: true,
title: const Text(
'Validation image',
style: TextStyle(color: Colors.white, fontSize: 18),
),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
actions: [
if (!_isLoading)
IconButton(
icon: const Icon(Icons.check),
onPressed: _onCropConfirm,
), ),
], body: Column(
),
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: [ children: [
const CircularProgressIndicator(color: Colors.white), // L'image capturée
const SizedBox(height: 16), Expanded(
Text( child: Container(
'Recadrage en cours...', margin: const EdgeInsets.all(20),
style: TextStyle(color: Colors.white.withValues(alpha: 0.8)), decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white10),
), ),
], child: ClipRRect(
), borderRadius: BorderRadius.circular(12),
);
}
return LayoutBuilder(
builder: (context, constraints) {
_viewportSize = Size(constraints.maxWidth, constraints.maxHeight);
// 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( child: Image.file(
File(widget.imagePath), File(widget.imagePath),
fit: BoxFit.contain, fit: BoxFit.contain,
width: _viewportSize.width,
height: _viewportSize.height,
), ),
), ),
), ),
), ),
// Overlay de recadrage // Bouton Modifier
Positioned.fill( Padding(
child: IgnorePointer( padding: const EdgeInsets.symmetric(vertical: 20),
child: CropOverlay(cropSize: _cropSize, showGrid: true), child: ElevatedButton.icon(
), onPressed: () {
), // Logique de modification à venir
// Instructions en bas
Positioned(
left: 0,
right: 0,
bottom: 24,
child: _buildInstructions(),
),
],
),
);
}, },
); icon: const Icon(Icons.crop_rotate),
} label: const Text('Modifier'),
style: ElevatedButton.styleFrom(
void _initializeImagePosition() { backgroundColor: const Color(0xFF1A73E8),
if (_imageSize == null) return; foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
final imageAspect = _imageSize!.width / _imageSize!.height; shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
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),
), ),
),
),
// Boutons du bas
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 30),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Expanded(
Icons.touch_app, child: OutlinedButton(
color: Colors.white.withValues(alpha: 0.8), onPressed: () => Navigator.pop(context),
size: 20, style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.white24),
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),
const SizedBox(width: 8), child: const Text('Recommencer', style: TextStyle(color: Colors.white)),
Text(
'Déplacez et zoomez pour cadrer la cible',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 14,
), ),
), ),
], const SizedBox(width: 16),
), Expanded(
); child: ElevatedButton(
} onPressed: () {
Navigator.push(
Future<void> _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, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => AnalysisScreen( builder: (_) => AnalysisScreen(
imagePath: croppedPath, imagePath: widget.imagePath,
targetType: widget.targetType, targetType: widget.targetType,
), ),
), ),
); );
} catch (e) { },
if (!mounted) return; style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A73E8),
setState(() { foregroundColor: Colors.white,
_isLoading = false; padding: const EdgeInsets.symmetric(vertical: 15),
}); shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
ScaffoldMessenger.of(context).showSnackBar( child: const Text('Valider', style: TextStyle(fontWeight: FontWeight.bold)),
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)),
);
}
} }