Ajout de la gestion des permissions photos et configuration native Android/iOS
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user