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,6 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<!-- Pour Android 12 et inférieur -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<!-- Pour Android 13 et supérieur -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<application <application
android:label="bully" android:label="bully"
android:name="${applicationName}" android:name="${applicationName}"

View File

@@ -2,8 +2,13 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>Bully a besoin d'accéder à vos photos pour analyser vos cibles enregistrées.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan documents</string> <string>Bully a besoin d'utiliser l'appareil photo pour scanner vos cibles en temps réel.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Bully a besoin d'accéder au micro (requis par certains services de caméra même si non utilisé).</string>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>

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 'dart:io';
import 'package:google_mlkit_document_scanner/google_mlkit_document_scanner.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.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/constants/app_constants.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../data/models/target_type.dart'; import '../../data/models/target_type.dart';
@@ -28,6 +24,61 @@ class _CaptureScreenState extends State<CaptureScreen> {
String? _selectedImagePath; String? _selectedImagePath;
bool _isLoading = false; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -39,7 +90,6 @@ class _CaptureScreenState extends State<CaptureScreen> {
children: [ children: [
const SizedBox(height: AppConstants.largePadding), const SizedBox(height: AppConstants.largePadding),
// Image source selection
_buildSectionTitle('Source de l\'Image'), _buildSectionTitle('Source de l\'Image'),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
@@ -58,14 +108,13 @@ class _CaptureScreenState extends State<CaptureScreen> {
label: 'Galerie', label: 'Galerie',
onPressed: _isLoading onPressed: _isLoading
? null ? null
: () => _captureImage(ImageSource.gallery), : _handleGallerySelection, // Appelle la nouvelle fonction
), ),
), ),
], ],
), ),
const SizedBox(height: AppConstants.largePadding), const SizedBox(height: AppConstants.largePadding),
// Image preview
if (_isLoading) if (_isLoading)
const Center( const Center(
child: Padding( child: Padding(
@@ -74,10 +123,9 @@ class _CaptureScreenState extends State<CaptureScreen> {
), ),
) )
else if (_selectedImagePath != null) else if (_selectedImagePath != null)
_buildImagePreview(), _buildImagePreview()
else
// Guide text _buildGuide(),
if (_selectedImagePath == null && !_isLoading) _buildGuide(),
], ],
), ),
), ),
@@ -91,6 +139,8 @@ class _CaptureScreenState extends State<CaptureScreen> {
); );
} }
// Tes widgets _buildSectionTitle, _buildImagePreview, _buildFramingHints restent identiques
Widget _buildSectionTitle(String title) { Widget _buildSectionTitle(String title) {
return Text( return Text(
title, title,
@@ -103,7 +153,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
Widget _buildImagePreview() { Widget _buildImagePreview() {
return Column( return Column(
children: [ children: [
_buildSectionTitle('Apercu'), _buildSectionTitle('Aperçu'),
const SizedBox(height: 12), const SizedBox(height: 12),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(AppConstants.borderRadius), borderRadius: BorderRadius.circular(AppConstants.borderRadius),
@@ -119,9 +169,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
right: 8, right: 8,
child: IconButton( child: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () => setState(() => _selectedImagePath = null),
setState(() => _selectedImagePath = null);
},
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.black54, backgroundColor: Colors.black54,
foregroundColor: Colors.white, foregroundColor: Colors.white,
@@ -139,7 +187,7 @@ class _CaptureScreenState extends State<CaptureScreen> {
Widget _buildFramingHints() { Widget _buildFramingHints() {
return Card( return Card(
color: AppTheme.warningColor.withValues(alpha: 0.1), color: AppTheme.warningColor.withOpacity(0.1),
child: Padding( child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Row( child: Row(
@@ -148,10 +196,8 @@ class _CaptureScreenState extends State<CaptureScreen> {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
'Assurez-vous que la cible est bien centree et visible.', 'Assurez-vous que la cible est bien centrée et visible.',
style: TextStyle( style: TextStyle(color: AppTheme.warningColor.withOpacity(0.8)),
color: AppTheme.warningColor.withValues(alpha: 0.8),
),
), ),
), ),
], ],
@@ -177,11 +223,11 @@ class _CaptureScreenState extends State<CaptureScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
_buildGuideItem( _buildGuideItem(
Icons.crop_free, 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.straighten, 'Prenez la photo de face'),
_buildGuideItem(Icons.blur_off, 'Evitez les images floues'), _buildGuideItem(Icons.blur_off, 'Évitez les images floues'),
_buildGuideItem( _buildGuideItem(
Icons.cleaning_services, Icons.cleaning_services,
'Nettoyer votre objectif avec une chiffonnette pour lunette si nécessaire', '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 { Future<void> _scanDocument() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final options = DocumentScannerOptions( final options = DocumentScannerOptions(
documentFormat: DocumentFormat.jpeg, documentFormat: DocumentFormat.jpeg,
@@ -215,32 +262,20 @@ class _CaptureScreenState extends State<CaptureScreen> {
pageLimit: 1, pageLimit: 1,
isGalleryImport: false, isGalleryImport: false,
); );
final scanner = DocumentScanner(options: options); final scanner = DocumentScanner(options: options);
final documents = await scanner.scanDocument(); final documents = await scanner.scanDocument();
if (documents.images.isNotEmpty) { if (documents.images.isNotEmpty) {
setState(() => _selectedImagePath = documents.images.first); setState(() => _selectedImagePath = documents.images.first);
} }
} catch (e) { } catch (e) {
if (mounted) { debugPrint('Erreur scan: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du scan: $e'),
backgroundColor: AppTheme.errorColor,
),
);
}
} finally { } finally {
if (mounted) { if (mounted) setState(() => _isLoading = false);
setState(() => _isLoading = false);
}
} }
} }
Future<void> _captureImage(ImageSource source) async { Future<void> _captureImage(ImageSource source) async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final XFile? image = await _picker.pickImage( final XFile? image = await _picker.pickImage(
source: source, source: source,
@@ -248,29 +283,18 @@ class _CaptureScreenState extends State<CaptureScreen> {
maxHeight: 2048, maxHeight: 2048,
imageQuality: 90, imageQuality: 90,
); );
if (image != null) { if (image != null) {
setState(() => _selectedImagePath = image.path); setState(() => _selectedImagePath = image.path);
} }
} catch (e) { } catch (e) {
if (mounted) { debugPrint('Erreur capture: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la capture: $e'),
backgroundColor: AppTheme.errorColor,
),
);
}
} finally { } finally {
if (mounted) { if (mounted) setState(() => _isLoading = false);
setState(() => _isLoading = false);
}
} }
} }
void _analyzeImage() { void _analyzeImage() {
if (_selectedImagePath == null) return; if (_selectedImagePath == null) return;
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(

View File

@@ -5,11 +5,13 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import device_info_plus
import file_selector_macos import file_selector_macos
import path_provider_foundation import path_provider_foundation
import sqflite_darwin import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))

View File

@@ -97,6 +97,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1+1" 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: equatable:
dependency: transitive dependency: transitive
description: description:
@@ -117,10 +133,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: file:
dependency: transitive dependency: transitive
description: description:
@@ -493,6 +517,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" 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: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -698,6 +770,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" 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: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@@ -65,6 +65,8 @@ dependencies:
# Image processing for impact detection # Image processing for impact detection
image: ^4.1.7 image: ^4.1.7
permission_handler: ^12.0.1
device_info_plus: ^13.1.0
# Machine Learning for YOLOv8 # Machine Learning for YOLOv8
# tflite_flutter: ^0.11.0 # tflite_flutter: ^0.11.0

View File

@@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
} }

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows file_selector_windows
permission_handler_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST