feat: intégration de la vue validation image (20)
This commit is contained in:
@@ -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)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user