7 Commits

13 changed files with 757 additions and 1178 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>
<key>NSCameraUsageDescription</key> <dict>
<string>This app needs camera access to scan documents</string> <key>NSPhotoLibraryUsageDescription</key>
<string>Bully a besoin d'accéder à vos photos pour analyser vos cibles enregistrées.</string>
<key>NSCameraUsageDescription</key>
<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,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'core/theme/app_theme.dart'; import 'core/theme/app_theme.dart';
import 'features/home/home_screen.dart'; import 'features/home/home_screen.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
class BullyApp extends StatelessWidget { class BullyApp extends StatelessWidget {
const BullyApp({super.key}); const BullyApp({super.key});
@@ -13,6 +14,16 @@ class BullyApp extends StatelessWidget {
theme: AppTheme.lightTheme, theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme, darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('fr', 'FR'), // Français
],
locale: const Locale('fr', 'FR'), // Force l'interface en français
home: const HomeScreen(), home: const HomeScreen(),
); );
} }

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';
@@ -24,14 +20,68 @@ class CaptureScreen extends StatefulWidget {
class _CaptureScreenState extends State<CaptureScreen> { class _CaptureScreenState extends State<CaptureScreen> {
final ImagePicker _picker = ImagePicker(); final ImagePicker _picker = ImagePicker();
TargetType _selectedType = TargetType.concentric; final TargetType _selectedType = TargetType.concentric;
String? _selectedImagePath; String? _selectedImagePath;
bool _isLoading = false; bool _isLoading = false;
/// Gère la demande de permission et la sélection d'image depuis la galerie
Future<void> _handleGallerySelection() async {
PermissionStatus status;
if (Platform.isAndroid) {
final deviceInfo = DeviceInfoPlugin();
final androidInfo = await deviceInfo.androidInfo;
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(
appBar: AppBar(title: const Text('Nouvelle Analyse')), appBar: AppBar(title: const Text('Source')),
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Column( child: Column(
@@ -39,7 +89,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(
@@ -56,16 +105,13 @@ class _CaptureScreenState extends State<CaptureScreen> {
child: ImageSourceButton( child: ImageSourceButton(
icon: Icons.photo_library, icon: Icons.photo_library,
label: 'Galerie', label: 'Galerie',
onPressed: _isLoading onPressed: _isLoading ? null : _handleGallerySelection,
? null
: () => _captureImage(ImageSource.gallery),
), ),
), ),
], ],
), ),
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,20 +120,44 @@ class _CaptureScreenState extends State<CaptureScreen> {
), ),
) )
else if (_selectedImagePath != null) else if (_selectedImagePath != null)
_buildImagePreview(), _buildImagePreview()
else
_buildGuide(),
// Guide text // --- NOUVEAU BOUTON VALIDER ---
if (_selectedImagePath == null && !_isLoading) _buildGuide(), if (_selectedImagePath != null && !_isLoading) ...[
const SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF112233), // Ton bleu foncé
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 54),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
onPressed: _analyzeImage,
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Valider',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 12),
Icon(Icons.arrow_forward, size: 20),
],
),
),
const SizedBox(height: 32),
],
], ],
), ),
), ),
floatingActionButton: _selectedImagePath != null
? FloatingActionButton.extended(
onPressed: _analyzeImage,
icon: const Icon(Icons.arrow_forward),
label: const Text('Suivant'),
)
: null,
); );
} }
@@ -103,7 +173,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 +189,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 +207,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 +216,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 +243,15 @@ 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(
Icons.cleaning_services,
'Nettoyer votre objectif avec une chiffonnette',
),
], ],
), ),
), ),
@@ -203,7 +273,6 @@ class _CaptureScreenState extends State<CaptureScreen> {
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,
@@ -211,32 +280,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,
@@ -244,29 +301,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

@@ -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: _buildBody(), body: Column(
); children: [
} // L'image capturée
Expanded(
Widget _buildBody() { child: Container(
if (!_imageLoaded || _imageSize == null) { margin: const EdgeInsets.all(20),
return const Center( decoration: BoxDecoration(
child: CircularProgressIndicator(color: Colors.white), borderRadius: BorderRadius.circular(12),
); border: Border.all(color: Colors.white10),
} ),
child: ClipRRect(
if (_isLoading) { borderRadius: BorderRadius.circular(12),
return Center( child: Image.file(
child: Column( File(widget.imagePath),
mainAxisAlignment: MainAxisAlignment.center, fit: BoxFit.contain,
children: [ ),
const CircularProgressIndicator(color: Colors.white), ),
const SizedBox(height: 16),
Text(
'Recadrage en cours...',
style: TextStyle(color: Colors.white.withValues(alpha: 0.8)),
), ),
], ),
),
);
}
return LayoutBuilder( // Bouton Modifier
builder: (context, constraints) { Padding(
_viewportSize = Size(constraints.maxWidth, constraints.maxHeight); padding: const EdgeInsets.symmetric(vertical: 20),
child: ElevatedButton.icon(
onPressed: () {
// Logique de modification à venir
},
icon: const Icon(Icons.crop_rotate),
label: const Text('Modifier'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A73E8),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
),
// Taille du carré de crop (90% de la plus petite dimension) // Boutons du bas
_cropSize = Padding(
math.min(constraints.maxWidth, constraints.maxHeight) * 0.85; padding: const EdgeInsets.fromLTRB(20, 0, 20, 30),
child: Row(
// Calculer l'échelle initiale si pas encore fait children: [
if (_scale == 1.0 && _offset == Offset.zero) { Expanded(
_initializeImagePosition(); child: OutlinedButton(
} onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
return GestureDetector( side: const BorderSide(color: Colors.white24),
onScaleStart: _onScaleStart, padding: const EdgeInsets.symmetric(vertical: 15),
onScaleUpdate: _onScaleUpdate, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
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(
File(widget.imagePath),
fit: BoxFit.contain,
width: _viewportSize.width,
height: _viewportSize.height,
), ),
child: const Text('Recommencer', style: TextStyle(color: Colors.white)),
), ),
), ),
), const SizedBox(width: 16),
Expanded(
// Overlay de recadrage child: ElevatedButton(
Positioned.fill( onPressed: () {
child: IgnorePointer( Navigator.push(
child: CropOverlay(cropSize: _cropSize, showGrid: true), context,
MaterialPageRoute(
builder: (_) => AnalysisScreen(
imagePath: widget.imagePath,
targetType: widget.targetType,
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A73E8),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Valider', style: TextStyle(fontWeight: FontWeight.bold)),
),
), ),
), ],
// Instructions en bas
Positioned(
left: 0,
right: 0,
bottom: 24,
child: _buildInstructions(),
),
],
),
);
},
);
}
void _initializeImagePosition() {
if (_imageSize == null) return;
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;
}
// É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),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.touch_app,
color: Colors.white.withValues(alpha: 0.8),
size: 20,
),
const SizedBox(width: 8),
Text(
'Déplacez et zoomez pour cadrer la cible',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 14,
), ),
), ),
], ],
), ),
); );
} }
}
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,
MaterialPageRoute(
builder: (_) => AnalysisScreen(
imagePath: croppedPath,
targetType: widget.targetType,
),
),
);
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
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)),
);
}
}

View File

@@ -1,10 +1,3 @@
/// Écran d'historique des sessions.
///
/// Affiche la liste des sessions passées avec filtrage par type de cible.
/// Inclut un graphique d'évolution des scores (10 dernières sessions)
/// et permet la navigation vers le détail de chaque session.
library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@@ -29,6 +22,9 @@ class _HistoryScreenState extends State<HistoryScreen> {
bool _isLoading = true; bool _isLoading = true;
TargetType? _filterType; TargetType? _filterType;
// --- MODIFICATION : Remplacement de DateTime par DateTimeRange ---
DateTimeRange? _selectedDateRange;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -36,17 +32,43 @@ class _HistoryScreenState extends State<HistoryScreen> {
} }
Future<void> _loadSessions() async { Future<void> _loadSessions() async {
if (!mounted) return;
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final repository = context.read<SessionRepository>(); final repository = context.read<SessionRepository>();
final sessions = await repository.getAllSessions( final sessions = await repository.getAllSessions(targetType: _filterType);
targetType: _filterType,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
_sessions = sessions; _sessions = sessions;
// --- LOGIQUE DE FILTRAGE PAR PÉRIODE ---
if (_selectedDateRange != null) {
_sessions = _sessions.where((s) {
// On compare uniquement les dates (sans les heures) pour éviter les bugs
final sessionDate = DateTime(
s.createdAt.year,
s.createdAt.month,
s.createdAt.day,
);
final startDate = DateTime(
_selectedDateRange!.start.year,
_selectedDateRange!.start.month,
_selectedDateRange!.start.day,
);
final endDate = DateTime(
_selectedDateRange!.end.year,
_selectedDateRange!.end.month,
_selectedDateRange!.end.day,
);
return sessionDate.isAtSameMomentAs(startDate) ||
sessionDate.isAtSameMomentAs(endDate) ||
(sessionDate.isAfter(startDate) &&
sessionDate.isBefore(endDate));
}).toList();
}
_isLoading = false; _isLoading = false;
}); });
} }
@@ -55,7 +77,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
setState(() => _isLoading = false); setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Erreur de chargement: $e'), content: Text('Erreur: $e'),
backgroundColor: AppTheme.errorColor, backgroundColor: AppTheme.errorColor,
), ),
); );
@@ -63,6 +85,35 @@ class _HistoryScreenState extends State<HistoryScreen> {
} }
} }
// --- MODIFICATION : Fonction DateRangePicker ---
Future<void> _pickDateRange() async {
final DateTimeRange? picked = await showDateRangePicker(
context: context,
initialDateRange: _selectedDateRange,
firstDate: DateTime(2020),
lastDate: DateTime.now().add(const Duration(days: 1)),
locale: const Locale('fr', 'FR'),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.dark(
primary: AppTheme.primaryColor,
onPrimary: Colors.white,
surface: Theme.of(context).cardColor,
onSurface: Colors.white,
),
),
child: child!,
);
},
);
if (picked != null) {
setState(() => _selectedDateRange = picked);
_loadSessions();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -71,55 +122,86 @@ class _HistoryScreenState extends State<HistoryScreen> {
actions: [ actions: [
PopupMenuButton<TargetType?>( PopupMenuButton<TargetType?>(
icon: const Icon(Icons.filter_list), icon: const Icon(Icons.filter_list),
tooltip: 'Filtrer',
onSelected: (type) { onSelected: (type) {
setState(() => _filterType = type); setState(() => _filterType = type);
_loadSessions(); _loadSessions();
}, },
itemBuilder: (context) => [ itemBuilder: (context) => [
const PopupMenuItem( const PopupMenuItem(value: null, child: Text('Tous')),
value: null, ...TargetType.values.map(
child: Text('Tous'), (type) =>
PopupMenuItem(value: type, child: Text(type.displayName)),
), ),
...TargetType.values.map((type) => PopupMenuItem(
value: type,
child: Text(type.displayName),
)),
], ],
), ),
], ],
), ),
body: _isLoading body: Column(
? const Center(child: CircularProgressIndicator()) children: [
: _sessions.isEmpty Expanded(
? _buildEmptyState() child: _isLoading
: _buildContent(), ? const Center(child: CircularProgressIndicator())
: _sessions.isEmpty
? _buildEmptyState()
: _buildContent(),
),
_buildBottomFilterBar(),
],
),
);
}
Widget _buildBottomFilterBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _pickDateRange,
icon: const Icon(Icons.date_range, size: 18),
label: Text(
_selectedDateRange == null
? 'Choisir une période'
: '${DateFormat('dd/MM/yy').format(_selectedDateRange!.start)} - ${DateFormat('dd/MM/yy').format(_selectedDateRange!.end)}',
),
),
),
if (_selectedDateRange != null)
IconButton(
icon: const Icon(Icons.close, color: AppTheme.errorColor),
onPressed: () {
setState(() => _selectedDateRange = null);
_loadSessions();
},
),
],
),
),
); );
} }
Widget _buildEmptyState() { Widget _buildEmptyState() {
return Center( return Center(
child: Padding( child: Column(
padding: const EdgeInsets.all(AppConstants.defaultPadding), mainAxisAlignment: MainAxisAlignment.center,
child: Column( children: [
mainAxisAlignment: MainAxisAlignment.center, Icon(Icons.history, size: 64, color: Colors.grey[400]),
children: [ const SizedBox(height: 16),
Icon(Icons.history, size: 64, color: Colors.grey[400]), const Text('Aucune session sur cette période'),
const SizedBox(height: 16), ],
Text(
'Aucune session',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
_filterType != null
? 'Aucune session de type ${_filterType!.displayName}'
: 'Commencez par analyser une cible',
style: TextStyle(color: Colors.grey[600]),
textAlign: TextAlign.center,
),
],
),
), ),
); );
} }
@@ -129,49 +211,27 @@ class _HistoryScreenState extends State<HistoryScreen> {
onRefresh: _loadSessions, onRefresh: _loadSessions,
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
// Chart section if (_sessions.length >= 2 && _selectedDateRange == null)
if (_sessions.length >= 2)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: HistoryChart(sessions: _sessions), child: HistoryChart(sessions: _sessions),
), ),
), ),
// Filter indicator
if (_filterType != null)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppConstants.defaultPadding),
child: Chip(
label: Text('Filtre: ${_filterType!.displayName}'),
deleteIcon: const Icon(Icons.close, size: 18),
onDeleted: () {
setState(() => _filterType = null);
_loadSessions();
},
),
),
),
// Sessions list
SliverPadding( SliverPadding(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(AppConstants.defaultPadding),
sliver: SliverList( sliver: SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate((context, index) {
(context, index) { final session = _sessions[index];
final session = _sessions[index]; return Padding(
return Padding( padding: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.only(bottom: 12), child: SessionListItem(
child: SessionListItem( session: session,
session: session, onTap: () => _openSessionDetail(session),
onTap: () => _openSessionDetail(session), onDelete: () => _deleteSession(session),
onDelete: () => _deleteSession(session), ),
), );
); }, childCount: _sessions.length),
},
childCount: _sessions.length,
),
), ),
), ),
], ],
@@ -182,11 +242,9 @@ class _HistoryScreenState extends State<HistoryScreen> {
void _openSessionDetail(Session session) async { void _openSessionDetail(Session session) async {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(builder: (_) => SessionDetailScreen(session: session)),
builder: (_) => SessionDetailScreen(session: session),
),
); );
_loadSessions(); // Refresh in case session was deleted _loadSessions();
} }
Future<void> _deleteSession(Session session) async { Future<void> _deleteSession(Session session) async {
@@ -204,7 +262,10 @@ class _HistoryScreenState extends State<HistoryScreen> {
), ),
TextButton( TextButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
child: const Text('Supprimer', style: TextStyle(color: AppTheme.errorColor)), child: const Text(
'Supprimer',
style: TextStyle(color: AppTheme.errorColor),
),
), ),
], ],
), ),
@@ -215,20 +276,8 @@ class _HistoryScreenState extends State<HistoryScreen> {
final repository = context.read<SessionRepository>(); final repository = context.read<SessionRepository>();
await repository.deleteSession(session.id); await repository.deleteSession(session.id);
_loadSessions(); _loadSessions();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Session supprimee')),
);
}
} catch (e) { } catch (e) {
if (mounted) { debugPrint('Erreur suppression: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: $e'),
backgroundColor: AppTheme.errorColor,
),
);
}
} }
} }
} }

View File

@@ -1,9 +1,3 @@
/// Écran d'accueil - Dashboard principal de l'application.
///
/// Affiche les statistiques globales (sessions, tirs, score moyen) et permet
/// la navigation vers les sections Statistiques, Historique et Nouvelle Analyse.
library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../core/constants/app_constants.dart'; import '../../core/constants/app_constants.dart';
@@ -46,6 +40,13 @@ class _HomeScreenState extends State<HomeScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// --- MODIFICATION 1 : AJOUT DE LA VERSION À GAUCHE ---
leading: const Center(
child: Text(
'v1.0.2',
style: TextStyle(fontSize: 12, color: Colors.white70),
),
),
title: const Text('Bully'), title: const Text('Bully'),
actions: [ actions: [
IconButton( IconButton(
@@ -94,7 +95,7 @@ class _HomeScreenState extends State<HomeScreen> {
Container( Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1), color: AppTheme.primaryColor.withOpacity(0.1),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Icon(
@@ -106,16 +107,16 @@ class _HomeScreenState extends State<HomeScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Analyse de Cibles', 'Analyse de Cibles',
style: Theme.of(context).textTheme.headlineMedium?.copyWith( style: Theme.of(
fontWeight: FontWeight.bold, context,
), ).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Scannez vos cibles et analysez vos performances', 'Scannez vos cibles et analysez vos performances',
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(
color: AppTheme.textSecondary, context,
), ).textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
@@ -147,14 +148,15 @@ class _HomeScreenState extends State<HomeScreen> {
children: [ children: [
Text( Text(
'Statistiques', 'Statistiques',
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(
fontWeight: FontWeight.bold, context,
), ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Première ligne de vignettes (Sessions et Tirs)
Row( Row(
children: [ children: [
// --- BOUTON SESSIONS (Redirige vers Statistiques) ---
Expanded( Expanded(
child: InkWell( child: InkWell(
onTap: () => _navigateToStatistics(context), onTap: () => _navigateToStatistics(context),
@@ -168,7 +170,6 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
// Ce bouton reste statique (ou tu peux ajouter une action)
Expanded( Expanded(
child: StatsCard( child: StatsCard(
icon: Icons.gps_fixed, icon: Icons.gps_fixed,
@@ -179,10 +180,32 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
], ],
), ),
const SizedBox(height: 12),
// --- MODIFICATION 2 : AJOUT DU GRAPHIQUE AU MILIEU ---
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Container(
height: 150,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Center(
child: Icon(Icons.show_chart, size: 50, color: Colors.grey),
),
),
),
// Deuxième ligne de vignettes (Historique et Meilleur)
Row( Row(
children: [ children: [
// --- BOUTON SCORE MOYEN (Redirige vers Historique) ---
Expanded( Expanded(
child: InkWell( child: InkWell(
onTap: () => _navigateToHistory(context), onTap: () => _navigateToHistory(context),
@@ -196,7 +219,6 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
// Ce bouton reste statique
Expanded( Expanded(
child: StatsCard( child: StatsCard(
icon: Icons.emoji_events, icon: Icons.emoji_events,
@@ -211,12 +233,13 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
// --- MÉTHODES DE NAVIGATION ---
void _navigateToCapture(BuildContext context) async { void _navigateToCapture(BuildContext context) async {
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute(builder: (_) => const CaptureScreen()), MaterialPageRoute(builder: (_) => const CaptureScreen()),
); );
// Refresh stats when returning
_loadStats(); _loadStats();
} }
@@ -225,7 +248,6 @@ class _HomeScreenState extends State<HomeScreen> {
context, context,
MaterialPageRoute(builder: (_) => const HistoryScreen()), MaterialPageRoute(builder: (_) => const HistoryScreen()),
); );
// Refresh stats when returning
_loadStats(); _loadStats();
} }
@@ -234,7 +256,6 @@ class _HomeScreenState extends State<HomeScreen> {
context, context,
MaterialPageRoute(builder: (_) => const StatisticsScreen()), MaterialPageRoute(builder: (_) => const StatisticsScreen()),
); );
// Refresh stats when returning
_loadStats(); _loadStats();
} }
} }

View File

@@ -1,21 +1,15 @@
/// Écran des statistiques détaillées. import 'dart:io';
///
/// Affiche les métriques de performance avec filtrage par période
/// (session, semaine, mois, tout). Inclut heat map, précision,
/// écart-type et distribution régionale des tirs.
library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:fl_chart/fl_chart.dart'; // La librairie pour les courbes
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/session.dart'; import '../../data/models/session.dart';
import '../../data/repositories/session_repository.dart'; import '../../data/repositories/session_repository.dart';
import '../../services/statistics_service.dart'; import '../../services/statistics_service.dart';
import 'widgets/heat_map_widget.dart';
class StatisticsScreen extends StatefulWidget { class StatisticsScreen extends StatefulWidget {
final Session? singleSession; // If provided, show stats for this session only final Session? singleSession;
const StatisticsScreen({super.key, this.singleSession}); const StatisticsScreen({super.key, this.singleSession});
@@ -30,720 +24,282 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
bool _isLoading = true; bool _isLoading = true;
List<Session> _allSessions = []; List<Session> _allSessions = [];
// Valeurs pour les Dropdowns (Filtres)
String _selectedWeapon = 'Toutes';
String _selectedDistance = 'Toutes';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Use addPostFrameCallback to ensure context is available WidgetsBinding.instance.addPostFrameCallback((_) => _loadStatistics());
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadStatistics();
});
} }
Future<void> _loadStatistics() async { Future<void> _loadStatistics() async {
if (!mounted) return; if (!mounted) return;
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
if (widget.singleSession != null) { final repository = context.read<SessionRepository>();
// Single session mode _allSessions = await repository.getAllSessions();
_statistics = _statisticsService.calculateStatistics( _calculateStats();
[widget.singleSession!],
period: StatsPeriod.session,
targetCenterX: widget.singleSession!.targetCenterX ?? 0.5,
targetCenterY: widget.singleSession!.targetCenterY ?? 0.5,
);
} else {
// Load all sessions
final repository = context.read<SessionRepository>();
_allSessions = await repository.getAllSessions();
_calculateStats();
}
} catch (e) { } catch (e) {
debugPrint('Error loading statistics: $e'); debugPrint('Error: $e');
} } finally {
if (mounted) setState(() => _isLoading = false);
if (mounted) {
setState(() => _isLoading = false);
} }
} }
void _calculateStats() { void _calculateStats() {
debugPrint(
'Calculating stats for ${_allSessions.length} sessions, period: $_selectedPeriod',
);
for (final session in _allSessions) {
debugPrint(
' Session: ${session.id}, shots: ${session.shots.length}, date: ${session.createdAt}',
);
}
_statistics = _statisticsService.calculateStatistics( _statistics = _statisticsService.calculateStatistics(
_allSessions, _allSessions,
period: _selectedPeriod, period: _selectedPeriod,
); );
debugPrint(
'Statistics result: totalShots=${_statistics?.totalShots}, totalScore=${_statistics?.totalScore}',
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: const Color(
0xFF121212,
), // Fond sombre comme sur le design
appBar: AppBar( appBar: AppBar(
title: Text( backgroundColor: Colors.transparent,
widget.singleSession != null elevation: 0,
? 'Statistiques Session' leading: IconButton(
: 'Statistiques', icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
), ),
title: const Text(
'Statistiques',
style: TextStyle(color: Colors.white),
),
centerTitle: true,
), ),
body: _isLoading body: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _statistics == null || _statistics!.totalShots == 0 : SingleChildScrollView(
? _buildEmptyState() padding: const EdgeInsets.all(16),
: _buildStatistics(), child: Column(
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.analytics_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucune donnee disponible',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Effectuez des sessions de tir pour voir vos statistiques',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey.shade600),
),
const SizedBox(height: 16),
Text(
'Sessions trouvees: ${_allSessions.length}',
style: TextStyle(color: Colors.grey.shade400, fontSize: 12),
),
if (_allSessions.isNotEmpty)
Text(
'Tirs totaux: ${_allSessions.fold<int>(0, (sum, s) => sum + s.shots.length)}',
style: TextStyle(color: Colors.grey.shade400, fontSize: 12),
),
],
),
),
);
}
Widget _buildStatistics() {
return RefreshIndicator(
onRefresh: _loadStatistics,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Period filter (only for multi-session view)
if (widget.singleSession == null) _buildPeriodFilter(),
const SizedBox(height: 16),
// Summary cards
_buildSummaryCards(),
const SizedBox(height: 24),
// Heat Map
_buildHeatMapSection(),
const SizedBox(height: 24),
// Precision stats
_buildPrecisionSection(),
const SizedBox(height: 24),
// Standard deviation
_buildStdDevSection(),
const SizedBox(height: 24),
// Regional distribution
_buildRegionalSection(),
],
),
),
);
}
Widget _buildPeriodFilter() {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Periode',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SegmentedButton<StatsPeriod>(
segments: const [
ButtonSegment(
value: StatsPeriod.week,
label: Text('7 jours'),
icon: Icon(Icons.date_range),
),
ButtonSegment(
value: StatsPeriod.month,
label: Text('30 jours'),
icon: Icon(Icons.calendar_month),
),
ButtonSegment(
value: StatsPeriod.all,
label: Text('Tout'),
icon: Icon(Icons.all_inclusive),
),
],
selected: {_selectedPeriod},
onSelectionChanged: (selection) {
setState(() {
_selectedPeriod = selection.first;
_calculateStats();
});
},
),
const SizedBox(height: 8),
Text(
'${_statistics!.sessions.length} session(s) - ${_statistics!.totalShots} tir(s)',
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
),
],
),
),
);
}
Widget _buildSummaryCards() {
return Row(
children: [
Expanded(
child: _StatCard(
icon: Icons.gps_fixed,
title: 'Tirs',
value: '${_statistics!.totalShots}',
color: AppTheme.primaryColor,
),
),
const SizedBox(width: 12),
Expanded(
child: _StatCard(
icon: Icons.score,
title: 'Score Total',
value: '${_statistics!.totalScore}',
color: AppTheme.secondaryColor,
),
),
],
);
}
Widget _buildHeatMapSection() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.grid_on, color: AppTheme.primaryColor),
const SizedBox(width: 8),
const Text(
'Zones Chaudes',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
const SizedBox(height: 8),
Text(
'Repartition de vos tirs sur la cible',
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
),
const SizedBox(height: 16),
Center(
child: HeatMapWidget(
heatMap: _statistics!.heatMap,
size: MediaQuery.of(context).size.width - 80,
),
),
const SizedBox(height: 12),
// Legend - gradient bar
Container(
height: 24,
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
gradient: const LinearGradient(
colors: [
Color(0xFF2196F3), // Blue (cold)
Color(0xFF00BCD4), // Cyan
Color(0xFFFFEB3B), // Yellow
Color(0xFFFF9800), // Orange
Color(0xFFFF1744), // Red (hot)
],
),
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 16),
child: Text(
'Peu',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
),
Padding(
padding: const EdgeInsets.only(right: 16),
child: Text(
'Beaucoup',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
),
],
),
],
),
),
);
}
Widget _buildPrecisionSection() {
final precision = _statistics!.precision;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.center_focus_strong,
color: AppTheme.successColor,
),
const SizedBox(width: 8),
const Text(
'Precision',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildPrecisionGauge(
'Precision',
precision.precisionScore,
'Distance moyenne du centre',
),
),
const SizedBox(width: 16),
Expanded(
child: _buildPrecisionGauge(
'Regularite',
precision.consistencyScore,
'Groupement des tirs',
),
),
],
),
const Divider(height: 32),
_buildStatRow(
'Distance moyenne du centre',
'${(precision.avgDistanceFromCenter * 100).toStringAsFixed(1)}%',
),
_buildStatRow(
'Diametre de groupement',
'${(precision.groupingDiameter * 100).toStringAsFixed(1)}%',
),
_buildStatRow(
'Score moyen',
_statistics!.avgScore.toStringAsFixed(2),
),
_buildStatRow('Meilleur score', '${_statistics!.maxScore}'),
_buildStatRow('Plus bas score', '${_statistics!.minScore}'),
],
),
),
);
}
Widget _buildPrecisionGauge(String title, double value, String subtitle) {
final color = value > 70
? AppTheme.successColor
: value > 40
? AppTheme.warningColor
: AppTheme.errorColor;
return Column(
children: [
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 80,
height: 80,
child: CircularProgressIndicator(
value: value / 100,
strokeWidth: 8,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation(color),
),
),
Text(
value.toStringAsFixed(0),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
const SizedBox(height: 8),
Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(
subtitle,
style: TextStyle(fontSize: 10, color: Colors.grey.shade600),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildStdDevSection() {
final stdDev = _statistics!.stdDev;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.stacked_line_chart,
color: AppTheme.warningColor,
),
const SizedBox(width: 8),
const Text(
'Ecart Type',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
const SizedBox(height: 8),
Text(
'Mesure de la dispersion de vos tirs',
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
),
const SizedBox(height: 16),
_buildStatRow(
'Ecart type X (horizontal)',
'${(stdDev.stdDevX * 100).toStringAsFixed(2)}%',
),
_buildStatRow(
'Ecart type Y (vertical)',
'${(stdDev.stdDevY * 100).toStringAsFixed(2)}%',
),
_buildStatRow(
'Ecart type radial',
'${(stdDev.stdDevRadial * 100).toStringAsFixed(2)}%',
),
_buildStatRow(
'Ecart type score',
stdDev.stdDevScore.toStringAsFixed(2),
),
const Divider(height: 24),
_buildStatRow(
'Position moyenne X',
'${(stdDev.meanX * 100).toStringAsFixed(1)}%',
),
_buildStatRow(
'Position moyenne Y',
'${(stdDev.meanY * 100).toStringAsFixed(1)}%',
),
_buildStatRow('Score moyen', stdDev.meanScore.toStringAsFixed(2)),
],
),
),
);
}
Widget _buildRegionalSection() {
final regional = _statistics!.regional;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.explore, color: AppTheme.secondaryColor),
const SizedBox(width: 8),
const Text(
'Distribution Regionale',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
const SizedBox(height: 16),
// Dominant direction
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [ children: [
const Icon( // 1. FILTRES (Arme et Distance)
Icons.compass_calibration, Row(
color: AppTheme.primaryColor, children: [
Expanded(
child: _buildDropdown('Arme utilisée', _selectedWeapon),
),
const SizedBox(width: 12),
Expanded(
child: _buildDropdown('Distance', _selectedDistance),
),
],
), ),
const SizedBox(width: 12), const SizedBox(height: 20),
Expanded(
child: Column( // 2. DONNÉES RAPIDES (Tirs et Sessions)
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ children: [
const Text('Direction dominante'), Expanded(
Text( child: _buildQuickStat(
regional.dominantDirection, 'Nbre de tirs',
style: const TextStyle( '${_statistics?.totalShots ?? 0}',
fontWeight: FontWeight.bold, Icons.gps_fixed,
fontSize: 18,
),
), ),
], ),
const SizedBox(width: 12),
Expanded(
child: _buildQuickStat(
'Nbre de sessions',
'${_statistics?.sessions.length ?? 0}',
Icons.analytics,
),
),
],
),
const SizedBox(height: 25),
// 3. LES GRAPHIQUES
_buildChartSection(
'Score',
'${_statistics?.totalScore ?? 0}',
[8, 12, 10, 15, 14, 18],
),
const SizedBox(height: 20),
_buildChartSection(
'Précision',
'${_statistics?.precision.precisionScore.toStringAsFixed(1)}%',
[60, 75, 70, 85, 80, 95],
),
const SizedBox(height: 20),
_buildChartSection('Groupement moyen', '14.2 mm', [
20,
18,
22,
15,
14,
12,
]), // mm fictif pour l'exemple
const SizedBox(height: 30),
// 4. BOUTON ACCUEIL
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A73E8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () => Navigator.of(
context,
).popUntil((route) => route.isFirst),
child: const Text(
'ACCUEIL',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
), ),
), ),
], ],
), ),
), ),
const SizedBox(height: 16),
// Bias
if (regional.biasX.abs() > 0.02 || regional.biasY.abs() > 0.02)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.warningColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(
Icons.warning_amber,
color: AppTheme.warningColor,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Biais detecte'),
Text(
_getBiasDescription(regional.biasX, regional.biasY),
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// Sector distribution
const Text(
'Repartition par secteur:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: regional.sectorDistribution.entries.map((entry) {
final percentage = _statistics!.totalShots > 0
? (entry.value / _statistics!.totalShots * 100)
: 0.0;
return _buildSectorChip(entry.key, entry.value, percentage);
}).toList(),
),
const SizedBox(height: 16),
// Quadrant distribution
const Text(
'Repartition par quadrant:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
_buildQuadrantGrid(regional.quadrantDistribution),
],
),
),
); );
} }
Widget _buildStatRow(String label, String value) { // Widget pour les Dropdowns de filtres
return Padding( Widget _buildDropdown(String label, String value) {
padding: const EdgeInsets.symmetric(vertical: 4), return Container(
child: Row( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
mainAxisAlignment: MainAxisAlignment.spaceBetween, decoration: BoxDecoration(
color: const Color(0xFF1E1E1E),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.white12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label), Text(
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)), label,
style: const TextStyle(color: Colors.white54, fontSize: 10),
),
DropdownButton<String>(
value: value,
isExpanded: true,
underline: Container(),
dropdownColor: const Color(0xFF1E1E1E),
style: const TextStyle(color: Colors.white, fontSize: 14),
items: [value]
.map(
(String val) =>
DropdownMenuItem(value: val, child: Text(val)),
)
.toList(),
onChanged: (newValue) {},
),
], ],
), ),
); );
} }
Widget _buildSectorChip(String sector, int count, double percentage) { // Widget pour les petites cartes de stats
Widget _buildQuickStat(String label, String value, IconData icon) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: count > 0 color: const Color(0xFF1E1E1E),
? AppTheme.primaryColor.withValues(alpha: 0.1) borderRadius: BorderRadius.circular(12),
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: count > 0 ? AppTheme.primaryColor : Colors.grey.shade300,
),
), ),
child: Text( child: Column(
'$sector: $count (${percentage.toStringAsFixed(0)}%)', children: [
style: TextStyle( Icon(icon, color: const Color(0xFF1A73E8), size: 20),
fontSize: 12, const SizedBox(height: 8),
color: count > 0 ? AppTheme.primaryColor : Colors.grey.shade600, Text(
), value,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: const TextStyle(color: Colors.white54, fontSize: 12),
),
],
), ),
); );
} }
Widget _buildQuadrantGrid(Map<String, int> quadrants) { // Widget pour une section de graphique
return Table( Widget _buildChartSection(
border: TableBorder.all(color: Colors.grey.shade300), String title,
children: [ String value,
TableRow( List<double> dataPoints,
children: [ ) {
_buildQuadrantCell('Haut-Gauche', quadrants['Haut-Gauche'] ?? 0), return Container(
_buildQuadrantCell('Haut-Droite', quadrants['Haut-Droite'] ?? 0), padding: const EdgeInsets.all(16),
], decoration: BoxDecoration(
), color: const Color(0xFF1E1E1E),
TableRow( borderRadius: BorderRadius.circular(12),
children: [ ),
_buildQuadrantCell('Bas-Gauche', quadrants['Bas-Gauche'] ?? 0), child: Column(
_buildQuadrantCell('Bas-Droite', quadrants['Bas-Droite'] ?? 0), crossAxisAlignment: CrossAxisAlignment.start,
], children: [
Text(
title,
style: const TextStyle(color: Colors.white70, fontSize: 14),
),
const SizedBox(height: 10),
Row(
children: [
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 20),
Expanded(
child: SizedBox(
height: 60,
child: LineChart(_mainChartData(dataPoints)),
),
),
],
),
],
),
);
}
LineChartData _mainChartData(List<double> points) {
return LineChartData(
gridData: const FlGridData(show: false),
titlesData: const FlTitlesData(show: false),
borderData: FlBorderData(show: false),
lineBarsData: [
LineChartBarData(
spots: points
.asMap()
.entries
.map((e) => FlSpot(e.key.toDouble(), e.value))
.toList(),
isCurved: true,
color: const Color(0xFF4CAF50), // Vert comme sur ton design
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
color: const Color(0xFF4CAF50).withOpacity(0.2),
),
), ),
], ],
); );
} }
Widget _buildQuadrantCell(String label, int count) {
final percentage = _statistics!.totalShots > 0
? (count / _statistics!.totalShots * 100)
: 0.0;
final intensity = _statistics!.totalShots > 0
? count / _statistics!.totalShots
: 0.0;
return Container(
padding: const EdgeInsets.all(16),
color: Color.lerp(Colors.white, AppTheme.primaryColor, intensity * 0.5),
child: Column(
children: [
Text(
'$count',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
),
Text(
'${percentage.toStringAsFixed(0)}%',
style: TextStyle(color: Colors.grey.shade600),
),
Text(
label,
style: const TextStyle(fontSize: 10),
textAlign: TextAlign.center,
),
],
),
);
}
String _getBiasDescription(double biasX, double biasY) {
final descriptions = <String>[];
if (biasX.abs() > 0.02) {
descriptions.add(biasX > 0 ? 'vers la droite' : 'vers la gauche');
}
if (biasY.abs() > 0.02) {
descriptions.add(biasY > 0 ? 'vers le bas' : 'vers le haut');
}
return 'Tendance ${descriptions.join(' et ')}';
}
}
class _StatCard extends StatelessWidget {
final IconData icon;
final String title;
final String value;
final Color color;
const _StatCard({
required this.icon,
required this.title,
required this.value,
required this.color,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(title, style: TextStyle(color: Colors.grey.shade600)),
],
),
),
);
}
} }

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

@@ -37,10 +37,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.0"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -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:
@@ -173,10 +197,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_chart name: fl_chart
sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d" sha256: b938f77d042cbcd822936a7a359a7235bad8bd72070de1f827efc2cc297ac888
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.66.2" version: "1.2.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -190,6 +214,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@@ -324,10 +353,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -372,26 +401,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.18" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" version: "0.11.1"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -488,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:
@@ -649,10 +726,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.9" version: "0.7.7"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -693,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:
@@ -718,5 +811,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.12.0-35.0.dev <4.0.0" dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.1" flutter: ">=3.38.1"

View File

@@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: ^3.12.0-35.0.dev sdk: '>=3.10.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions
@@ -31,6 +31,8 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
@@ -56,13 +58,15 @@ dependencies:
uuid: ^4.3.3 uuid: ^4.3.3
# Date formatting # Date formatting
intl: ^0.19.0 intl: ^0.20.2
# Charts for history visualization # Charts for history visualization
fl_chart: ^0.66.2 fl_chart: ^1.2.0
# 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