diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f7b80a6a..f8435084 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,11 @@ - + + + + + + - NSCameraUsageDescription - This app needs camera access to scan documents + + NSPhotoLibraryUsageDescription + Bully a besoin d'accéder à vos photos pour analyser vos cibles enregistrées. + NSCameraUsageDescription + Bully a besoin d'utiliser l'appareil photo pour scanner vos cibles en temps réel. + NSMicrophoneUsageDescription + Bully a besoin d'accéder au micro (requis par certains services de caméra même si non utilisé). CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index bc0de3e9..5a53be8a 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -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 { String? _selectedImagePath; bool _isLoading = false; + /// Gère la demande de permission et la sélection d'image + Future _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 { 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 { 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 { ), ) else if (_selectedImagePath != null) - _buildImagePreview(), - - // Guide text - if (_selectedImagePath == null && !_isLoading) _buildGuide(), + _buildImagePreview() + else + _buildGuide(), ], ), ), @@ -91,6 +139,8 @@ class _CaptureScreenState extends State { ); } + // Tes widgets _buildSectionTitle, _buildImagePreview, _buildFramingHints restent identiques + Widget _buildSectionTitle(String title) { return Text( title, @@ -103,7 +153,7 @@ class _CaptureScreenState extends State { 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 { 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 { 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 { 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 { 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 { ); } + // --- LOGIQUE DE CAPTURE --- + Future _scanDocument() async { setState(() => _isLoading = true); - try { final options = DocumentScannerOptions( documentFormat: DocumentFormat.jpeg, @@ -215,32 +262,20 @@ class _CaptureScreenState extends State { 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 _captureImage(ImageSource source) async { setState(() => _isLoading = true); - try { final XFile? image = await _picker.pickImage( source: source, @@ -248,29 +283,18 @@ class _CaptureScreenState extends State { 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( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0c9779f0..54d40f3a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,13 @@ import FlutterMacOS import Foundation +import device_info_plus import file_selector_macos import path_provider_foundation import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/pubspec.lock b/pubspec.lock index 8453b3e2..c7627e42 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1+1" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "6a642e1daa10190af89ba6cb6386c0df7d071a3592080bfe1e44faa63ae1df65" + url: "https://pub.dev" + source: hosted + version: "13.1.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46" + url: "https://pub.dev" + source: hosted + version: "8.1.0" equatable: dependency: transitive description: @@ -117,10 +133,18 @@ packages: dependency: transitive description: name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" file: dependency: transitive description: @@ -493,6 +517,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -698,6 +770,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: ba7d5750e3441caa1bbe31d9e516348fcf8dfcb32aa29ef87a844a59f4d1f1d0 + url: "https://pub.dev" + source: hosted + version: "6.1.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904" + url: "https://pub.dev" + source: hosted + version: "3.0.3" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b17c9a91..fe872042 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,8 @@ dependencies: # Image processing for impact detection image: ^4.1.7 + permission_handler: ^12.0.1 + device_info_plus: ^13.1.0 # Machine Learning for YOLOv8 # tflite_flutter: ^0.11.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 77ab7a09..2c256bd4 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a423a024..230eabf6 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + permission_handler_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST