Ajout de la gestion des permissions photos et configuration native Android/iOS

This commit is contained in:
qguillaume
2026-05-05 18:05:49 +02:00
parent 55f6210c97
commit d98f54cf93
8 changed files with 190 additions and 60 deletions

View File

@@ -1,14 +1,10 @@
/// Écran de capture - Première étape du workflow d'analyse.
///
/// Permet de sélectionner le type de cible (concentrique ou silhouette)
/// et la source d'image (caméra ou galerie). Affiche un aperçu de l'image
/// sélectionnée avant de lancer l'analyse.
library;
import 'dart:io';
import 'package:google_mlkit_document_scanner/google_mlkit_document_scanner.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:google_mlkit_document_scanner/google_mlkit_document_scanner.dart';
import 'package:device_info_plus/device_info_plus.dart';
import '../../core/constants/app_constants.dart';
import '../../core/theme/app_theme.dart';
import '../../data/models/target_type.dart';
@@ -28,6 +24,61 @@ class _CaptureScreenState extends State<CaptureScreen> {
String? _selectedImagePath;
bool _isLoading = false;
/// Gère la demande de permission et la sélection d'image
Future<void> _handleGallerySelection() async {
PermissionStatus status;
if (Platform.isAndroid) {
final deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
// Android 13+ (SDK 33) utilise .photos, les versions précédentes utilisent .storage
if (androidInfo.version.sdkInt >= 33) {
status = await Permission.photos.request();
} else {
status = await Permission.storage.request();
}
} else {
status = await Permission.photos.request();
}
if (status.isGranted) {
_captureImage(ImageSource.gallery);
} else if (status.isPermanentlyDenied) {
_showSettingsDialog();
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Accès à la galerie requis pour continuer'),
),
);
}
}
}
void _showSettingsDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Permission requise'),
content: const Text(
'L\'accès aux photos est nécessaire. Veuillez l\'activer dans les paramètres.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => openAppSettings(),
child: const Text('Paramètres'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -39,7 +90,6 @@ class _CaptureScreenState extends State<CaptureScreen> {
children: [
const SizedBox(height: AppConstants.largePadding),
// Image source selection
_buildSectionTitle('Source de l\'Image'),
const SizedBox(height: 12),
Row(
@@ -58,14 +108,13 @@ class _CaptureScreenState extends State<CaptureScreen> {
label: 'Galerie',
onPressed: _isLoading
? null
: () => _captureImage(ImageSource.gallery),
: _handleGallerySelection, // Appelle la nouvelle fonction
),
),
],
),
const SizedBox(height: AppConstants.largePadding),
// Image preview
if (_isLoading)
const Center(
child: Padding(
@@ -74,10 +123,9 @@ class _CaptureScreenState extends State<CaptureScreen> {
),
)
else if (_selectedImagePath != null)
_buildImagePreview(),
// Guide text
if (_selectedImagePath == null && !_isLoading) _buildGuide(),
_buildImagePreview()
else
_buildGuide(),
],
),
),
@@ -91,6 +139,8 @@ class _CaptureScreenState extends State<CaptureScreen> {
);
}
// Tes widgets _buildSectionTitle, _buildImagePreview, _buildFramingHints restent identiques
Widget _buildSectionTitle(String title) {
return Text(
title,
@@ -103,7 +153,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
Widget _buildImagePreview() {
return Column(
children: [
_buildSectionTitle('Apercu'),
_buildSectionTitle('Aperçu'),
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
@@ -119,9 +169,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
right: 8,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() => _selectedImagePath = null);
},
onPressed: () => setState(() => _selectedImagePath = null),
style: IconButton.styleFrom(
backgroundColor: Colors.black54,
foregroundColor: Colors.white,
@@ -139,7 +187,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
Widget _buildFramingHints() {
return Card(
color: AppTheme.warningColor.withValues(alpha: 0.1),
color: AppTheme.warningColor.withOpacity(0.1),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
@@ -148,10 +196,8 @@ class _CaptureScreenState extends State<CaptureScreen> {
const SizedBox(width: 12),
Expanded(
child: Text(
'Assurez-vous que la cible est bien centree et visible.',
style: TextStyle(
color: AppTheme.warningColor.withValues(alpha: 0.8),
),
'Assurez-vous que la cible est bien centrée et visible.',
style: TextStyle(color: AppTheme.warningColor.withOpacity(0.8)),
),
),
],
@@ -177,11 +223,11 @@ class _CaptureScreenState extends State<CaptureScreen> {
const SizedBox(height: 12),
_buildGuideItem(
Icons.crop_free,
'Cadrez la cible entiere dans l\'image',
'Cadrez la cible entière dans l\'image',
),
_buildGuideItem(Icons.wb_sunny, 'Utilisez un bon eclairage'),
_buildGuideItem(Icons.wb_sunny, 'Utilisez un bon éclairage'),
_buildGuideItem(Icons.straighten, 'Prenez la photo de face'),
_buildGuideItem(Icons.blur_off, 'Evitez les images floues'),
_buildGuideItem(Icons.blur_off, 'Évitez les images floues'),
_buildGuideItem(
Icons.cleaning_services,
'Nettoyer votre objectif avec une chiffonnette pour lunette si nécessaire',
@@ -205,9 +251,10 @@ class _CaptureScreenState extends State<CaptureScreen> {
);
}
// --- LOGIQUE DE CAPTURE ---
Future<void> _scanDocument() async {
setState(() => _isLoading = true);
try {
final options = DocumentScannerOptions(
documentFormat: DocumentFormat.jpeg,
@@ -215,32 +262,20 @@ class _CaptureScreenState extends State<CaptureScreen> {
pageLimit: 1,
isGalleryImport: false,
);
final scanner = DocumentScanner(options: options);
final documents = await scanner.scanDocument();
if (documents.images.isNotEmpty) {
setState(() => _selectedImagePath = documents.images.first);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du scan: $e'),
backgroundColor: AppTheme.errorColor,
),
);
}
debugPrint('Erreur scan: $e');
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _captureImage(ImageSource source) async {
setState(() => _isLoading = true);
try {
final XFile? image = await _picker.pickImage(
source: source,
@@ -248,29 +283,18 @@ class _CaptureScreenState extends State<CaptureScreen> {
maxHeight: 2048,
imageQuality: 90,
);
if (image != null) {
setState(() => _selectedImagePath = image.path);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la capture: $e'),
backgroundColor: AppTheme.errorColor,
),
);
}
debugPrint('Erreur capture: $e');
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
if (mounted) setState(() => _isLoading = false);
}
}
void _analyzeImage() {
if (_selectedImagePath == null) return;
Navigator.push(
context,
MaterialPageRoute(