Compare commits
9 Commits
v.0.0.3
...
feature/gl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8946955f76 | ||
|
|
be62669d56 | ||
|
|
db6dd0cee5 | ||
|
|
d98f54cf93 | ||
|
|
55f6210c97 | ||
|
|
8d9b35104a | ||
|
|
5cc67228e4 | ||
|
|
105eb1cab0 | ||
|
|
fba3b41f2f |
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"avdmanager.executable": "c:\\Users\\streaper2\\AppData\\Local\\Android\\Sdk\\cmdline-tools\\latest\\bin\\avdmanager.bat",
|
||||||
|
"avdmanager.sdkManager": "c:\\Users\\streaper2\\AppData\\Local\\Android\\Sdk\\cmdline-tools\\latest\\bin\\sdkmanager.bat"
|
||||||
|
}
|
||||||
BIN
analyze_output.txt
Normal file
BIN
analyze_output.txt
Normal file
Binary file not shown.
BIN
analyze_results_final.txt
Normal file
BIN
analyze_results_final.txt
Normal file
Binary file not shown.
37
analyze_results_fixed.txt
Normal file
37
analyze_results_fixed.txt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
Analyzing bully...
|
||||||
|
|
||||||
|
info - Don't invoke 'print' in production code - lib\features\analysis\analysis_provider.dart:553:7 - avoid_print
|
||||||
|
info - The private field _selectedType could be 'final' - lib\features\capture\capture_screen.dart:27:14 - prefer_final_fields
|
||||||
|
info - 'scale' is deprecated and shouldn't be used. Use scaleByVector3, scaleByVector4, or scaleByDouble instead - lib\features\crop\crop_screen.dart:142:25 - deprecated_member_use
|
||||||
|
info - Statements in an if should be enclosed in a block - lib\services\distortion_correction_service.dart:566:11 - curly_braces_in_flow_control_structures
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:639:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:764:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:825:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:953:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:1015:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:1063:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:192:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:239:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:246:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:278:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:289:11 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:292:11 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:297:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:332:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:336:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:683:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:725:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:736:7 - avoid_print
|
||||||
|
warning - The declaration '_detectDarkSpotsAdaptive' isn't referenced - lib\services\image_processing_service.dart:780:15 - unused_element
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:328:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:377:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:414:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:23:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:29:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:31:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:67:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:4:3 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:5:3 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:6:3 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - tests\test_homography.dart:4:3 - avoid_print
|
||||||
|
|
||||||
38
analyze_utf8.txt
Normal file
38
analyze_utf8.txt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
Analyzing bully...
|
||||||
|
|
||||||
|
info - Don't invoke 'print' in production code - lib\features\analysis\analysis_provider.dart:392:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\features\analysis\analysis_provider.dart:596:7 - avoid_print
|
||||||
|
info - The private field _selectedType could be 'final' - lib\features\capture\capture_screen.dart:27:14 - prefer_final_fields
|
||||||
|
info - 'scale' is deprecated and shouldn't be used. Use scaleByVector3, scaleByVector4, or scaleByDouble instead - lib\features\crop\crop_screen.dart:142:25 - deprecated_member_use
|
||||||
|
info - Statements in an if should be enclosed in a block - lib\services\distortion_correction_service.dart:566:11 - curly_braces_in_flow_control_structures
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:639:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:764:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:825:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:953:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:1015:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\distortion_correction_service.dart:1063:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:192:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:239:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:246:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:278:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:289:11 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:292:11 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:297:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:332:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:336:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:683:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:725:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\image_processing_service.dart:736:7 - avoid_print
|
||||||
|
warning - The declaration '_detectDarkSpotsAdaptive' isn't referenced - lib\services\image_processing_service.dart:780:15 - unused_element
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:328:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:377:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\target_detection_service.dart:414:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:23:9 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:29:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:31:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - lib\services\yolo_impact_detection_service.dart:67:7 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:4:3 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:5:3 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - tests\opencv_quad_test.dart:6:3 - avoid_print
|
||||||
|
info - Don't invoke 'print' in production code - tests\test_homography.dart:4:3 - avoid_print
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ android {
|
|||||||
// TODO: Add your own signing config for the release build.
|
// TODO: Add your own signing config for the release build.
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
android/app/proguard-rules.pro
vendored
Normal file
7
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# TensorFlow Lite
|
||||||
|
-keep class org.tensorflow.lite.** { *; }
|
||||||
|
-keep class com.google.android.gms.tflite.** { *; }
|
||||||
|
-dontwarn org.tensorflow.lite.**
|
||||||
|
|
||||||
|
# Specifically for the GPU delegate error
|
||||||
|
-dontwarn org.tensorflow.lite.gpu.GpuDelegateFactory$Options
|
||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
11
lib/app.dart
11
lib/app.dart
@@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,6 +223,16 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update a shot's score manually
|
||||||
|
void updateShotScore(String shotId, int newScore) {
|
||||||
|
final index = _shots.indexWhere((shot) => shot.id == shotId);
|
||||||
|
if (index == -1) return;
|
||||||
|
|
||||||
|
_shots[index] = _shots[index].copyWith(score: newScore);
|
||||||
|
_recalculateScores();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Auto-detect impacts using image processing
|
/// Auto-detect impacts using image processing
|
||||||
Future<int> autoDetectImpacts({
|
Future<int> autoDetectImpacts({
|
||||||
int darkThreshold = 80,
|
int darkThreshold = 80,
|
||||||
@@ -293,6 +303,8 @@ class AnalysisProvider extends ChangeNotifier {
|
|||||||
double param2 = 30,
|
double param2 = 30,
|
||||||
int minRadius = 5,
|
int minRadius = 5,
|
||||||
int maxRadius = 50,
|
int maxRadius = 50,
|
||||||
|
int minSize = 5,
|
||||||
|
int maxSize = 1000,
|
||||||
int blurSize = 5,
|
int blurSize = 5,
|
||||||
bool useContourDetection = true,
|
bool useContourDetection = true,
|
||||||
double minCircularity = 0.6,
|
double minCircularity = 0.6,
|
||||||
|
|||||||
@@ -274,68 +274,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Auto-calibrate button
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () async {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Text('Auto-calibration en cours...'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
duration: Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final success = await provider
|
|
||||||
.autoCalibrateTarget();
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).hideCurrentSnackBar();
|
|
||||||
if (success) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'Cible calibrée automatiquement',
|
|
||||||
),
|
|
||||||
backgroundColor: AppTheme.successColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'Échec de la calibration auto',
|
|
||||||
),
|
|
||||||
backgroundColor: AppTheme.errorColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.auto_fix_high),
|
|
||||||
label: const Text('Auto-Calibrer la Cible'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.deepPurple,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
// Ring count slider
|
// Ring count slider
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -387,57 +326,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Target size slider
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.zoom_out_map,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Text(
|
|
||||||
'Taille:',
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Slider(
|
|
||||||
value: provider.targetRadius.clamp(0.05, 3.0),
|
|
||||||
min: 0.05,
|
|
||||||
max: 3.0,
|
|
||||||
label:
|
|
||||||
'${(provider.targetRadius * 100).toStringAsFixed(0)}%',
|
|
||||||
activeColor: AppTheme.warningColor,
|
|
||||||
onChanged: (value) {
|
|
||||||
provider.adjustTargetPosition(
|
|
||||||
provider.targetCenterX,
|
|
||||||
provider.targetCenterY,
|
|
||||||
provider.targetInnerRadius,
|
|
||||||
value,
|
|
||||||
ringCount: provider.ringCount,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.warningColor,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'${(provider.targetRadius * 100).toStringAsFixed(0)}%',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(color: Colors.white24, height: 16),
|
const Divider(color: Colors.white24, height: 16),
|
||||||
// Distortion correction row
|
// Distortion correction row
|
||||||
/*Row(
|
/*Row(
|
||||||
@@ -793,6 +681,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
// Overlay qui se transforme avec l'image
|
// Overlay qui se transforme avec l'image
|
||||||
TargetOverlay(
|
TargetOverlay(
|
||||||
|
showRings: true,
|
||||||
shots: provider.shots,
|
shots: provider.shots,
|
||||||
targetCenterX: provider.targetCenterX,
|
targetCenterX: provider.targetCenterX,
|
||||||
targetCenterY: provider.targetCenterY,
|
targetCenterY: provider.targetCenterY,
|
||||||
@@ -879,6 +768,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
),
|
),
|
||||||
// Overlay qui se transforme avec l'image
|
// Overlay qui se transforme avec l'image
|
||||||
TargetOverlay(
|
TargetOverlay(
|
||||||
|
showRings: true,
|
||||||
shots: provider.shots,
|
shots: provider.shots,
|
||||||
targetCenterX: provider.targetCenterX,
|
targetCenterX: provider.targetCenterX,
|
||||||
targetCenterY: provider.targetCenterY,
|
targetCenterY: provider.targetCenterY,
|
||||||
@@ -991,107 +881,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) {
|
Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) {
|
||||||
return Column(
|
return const SizedBox.shrink();
|
||||||
children: [
|
|
||||||
// Reference-based detection section
|
|
||||||
if (_isSelectingReferences) ...[
|
|
||||||
Card(
|
|
||||||
color: Colors.deepPurple.withValues(alpha: 0.1),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.touch_app, color: Colors.deepPurple),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'${provider.referenceImpacts.length} reference(s) selectionnee(s)',
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
const Text(
|
|
||||||
'Touchez l\'image pour marquer 3-4 impacts de reference. '
|
|
||||||
'L\'algorithme apprendra leurs caracteristiques pour detecter les autres.',
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: () {
|
|
||||||
setState(() => _isSelectingReferences = false);
|
|
||||||
provider.clearReferenceImpacts();
|
|
||||||
},
|
|
||||||
child: const Text('Annuler'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: provider.referenceImpacts.length >= 2
|
|
||||||
? () => _showCalibratedDetectionDialog(
|
|
||||||
context,
|
|
||||||
provider,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
icon: const Icon(Icons.auto_fix_high),
|
|
||||||
label: const Text('Detecter'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.deepPurple,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
] else ...[
|
|
||||||
// désactiver le temps de l'amelioration du scripts d'auto-detection
|
|
||||||
// Auto-detect buttons row
|
|
||||||
// Row(
|
|
||||||
// children: [
|
|
||||||
// Expanded(
|
|
||||||
// child: ElevatedButton.icon(
|
|
||||||
// onPressed: () => _showAutoDetectDialog(context, provider),
|
|
||||||
// icon: const Icon(Icons.auto_fix_high),
|
|
||||||
// label: const Text('Auto-Detection'),
|
|
||||||
// style: ElevatedButton.styleFrom(
|
|
||||||
// backgroundColor: AppTheme.primaryColor,
|
|
||||||
// foregroundColor: Colors.white,
|
|
||||||
// padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(width: 12),
|
|
||||||
// Expanded(
|
|
||||||
// child: ElevatedButton.icon(
|
|
||||||
// onPressed: () => setState(() => _isSelectingReferences = true),
|
|
||||||
// icon: const Icon(Icons.touch_app),
|
|
||||||
// label: const Text('Par Reference'),
|
|
||||||
// style: ElevatedButton.styleFrom(
|
|
||||||
// backgroundColor: Colors.deepPurple,
|
|
||||||
// foregroundColor: Colors.white,
|
|
||||||
// padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Manual actions
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showHelpDialog(BuildContext context) {
|
void _showHelpDialog(BuildContext context) {
|
||||||
@@ -1129,21 +919,68 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
AnalysisProvider provider,
|
AnalysisProvider provider,
|
||||||
String shotId,
|
String shotId,
|
||||||
) {
|
) {
|
||||||
|
final shot = provider.shots.firstWhere((s) => s.id == shotId);
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
builder: (context) => SafeArea(
|
builder: (context) => SafeArea(
|
||||||
child: Wrap(
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
Text(
|
||||||
leading: const Icon(Icons.delete, color: AppTheme.errorColor),
|
'Modifier l\'impact',
|
||||||
title: const Text('Supprimer cet impact'),
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
onTap: () {
|
),
|
||||||
provider.removeShot(shotId);
|
const SizedBox(height: 20),
|
||||||
|
// Dropdown for score
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text('Valeur de l\'impact : ', style: TextStyle(fontSize: 16)),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
DropdownButton<int>(
|
||||||
|
value: shot.score.clamp(0, 10),
|
||||||
|
items: List.generate(11, (i) => i).map((i) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
value: i,
|
||||||
|
child: Text(i == 10 && provider.targetType == TargetType.concentric ? '10 (X)' : '$i'),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (newScore) {
|
||||||
|
if (newScore != null) {
|
||||||
|
provider.updateShotScore(shotId, newScore);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Delete button at the bottom
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
provider.removeShot(shotId);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
label: const Text('SUPPRIMER L\'IMPACT'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.errorColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1187,7 +1024,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
void _showAutoDetectDialog(BuildContext context, AnalysisProvider provider) {
|
void _showAutoDetectDialog(BuildContext context, AnalysisProvider provider) {
|
||||||
// Detection settings
|
// Detection settings
|
||||||
bool clearExisting = true;
|
bool clearExisting = true;
|
||||||
@@ -1197,9 +1034,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
int maxImpactSize = 500;
|
int maxImpactSize = 500;
|
||||||
double minFillRatio = 0.5;
|
double minFillRatio = 0.5;
|
||||||
|
|
||||||
// NOTE: OpenCV désactivé - problèmes de build Windows
|
|
||||||
// Utilisation de la détection classique uniquement
|
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => StatefulBuilder(
|
builder: (context) => StatefulBuilder(
|
||||||
@@ -1216,11 +1050,12 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
const Text(
|
const Text(
|
||||||
'Ajustez les parametres de detection:',
|
'Détection Classique (Paramétrable):',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// Dark threshold slider
|
// Dark threshold slider
|
||||||
Text('Seuil de detection (zones sombres): $darkThreshold'),
|
Text('Seuil de detection (zones sombres): $darkThreshold'),
|
||||||
@@ -1379,14 +1214,13 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
label: const Text('Detecter'),
|
label: const Text('Détecter (Classique)'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
void _showCalibratedDetectionDialog(
|
void _showCalibratedDetectionDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class TargetOverlay extends StatelessWidget {
|
|||||||
final double? groupingDiameter;
|
final double? groupingDiameter;
|
||||||
final List<Shot>? referenceImpacts;
|
final List<Shot>? referenceImpacts;
|
||||||
final double zoomScale;
|
final double zoomScale;
|
||||||
|
final bool showRings;
|
||||||
|
|
||||||
const TargetOverlay({
|
const TargetOverlay({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -42,6 +43,7 @@ class TargetOverlay extends StatelessWidget {
|
|||||||
this.groupingDiameter,
|
this.groupingDiameter,
|
||||||
this.referenceImpacts,
|
this.referenceImpacts,
|
||||||
this.zoomScale = 1.0,
|
this.zoomScale = 1.0,
|
||||||
|
this.showRings = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -72,6 +74,7 @@ class TargetOverlay extends StatelessWidget {
|
|||||||
groupingDiameter: groupingDiameter,
|
groupingDiameter: groupingDiameter,
|
||||||
referenceImpacts: referenceImpacts,
|
referenceImpacts: referenceImpacts,
|
||||||
zoomScale: zoomScale,
|
zoomScale: zoomScale,
|
||||||
|
showRings: showRings,
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: shots.map((shot) {
|
children: shots.map((shot) {
|
||||||
@@ -132,6 +135,7 @@ class _TargetOverlayPainter extends CustomPainter {
|
|||||||
final double? groupingDiameter;
|
final double? groupingDiameter;
|
||||||
final List<Shot>? referenceImpacts;
|
final List<Shot>? referenceImpacts;
|
||||||
final double zoomScale;
|
final double zoomScale;
|
||||||
|
final bool showRings;
|
||||||
|
|
||||||
_TargetOverlayPainter({
|
_TargetOverlayPainter({
|
||||||
required this.shots,
|
required this.shots,
|
||||||
@@ -146,12 +150,15 @@ class _TargetOverlayPainter extends CustomPainter {
|
|||||||
this.groupingDiameter,
|
this.groupingDiameter,
|
||||||
this.referenceImpacts,
|
this.referenceImpacts,
|
||||||
this.zoomScale = 1.0,
|
this.zoomScale = 1.0,
|
||||||
|
this.showRings = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
// Draw target center indicator
|
// Draw target center indicator
|
||||||
|
if (showRings) {
|
||||||
_drawTargetCenter(canvas, size);
|
_drawTargetCenter(canvas, size);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw grouping circle
|
// Draw grouping circle
|
||||||
if (groupingCenterX != null && groupingCenterY != null && groupingDiameter != null && shots.length > 1) {
|
if (groupingCenterX != null && groupingCenterY != null && groupingDiameter != null && shots.length > 1) {
|
||||||
@@ -371,6 +378,7 @@ class _TargetOverlayPainter extends CustomPainter {
|
|||||||
groupingCenterY != oldDelegate.groupingCenterY ||
|
groupingCenterY != oldDelegate.groupingCenterY ||
|
||||||
groupingDiameter != oldDelegate.groupingDiameter ||
|
groupingDiameter != oldDelegate.groupingDiameter ||
|
||||||
referenceImpacts != oldDelegate.referenceImpacts ||
|
referenceImpacts != oldDelegate.referenceImpacts ||
|
||||||
zoomScale != oldDelegate.zoomScale;
|
zoomScale != oldDelegate.zoomScale ||
|
||||||
|
showRings != oldDelegate.showRings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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: Column(
|
||||||
),
|
|
||||||
body: _buildBody(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBody() {
|
|
||||||
if (!_imageLoaded || _imageSize == null) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(color: Colors.white),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isLoading) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
const CircularProgressIndicator(color: Colors.white),
|
// L'image capturée
|
||||||
const SizedBox(height: 16),
|
Expanded(
|
||||||
Text(
|
child: Container(
|
||||||
'Recadrage en cours...',
|
margin: const EdgeInsets.all(20),
|
||||||
style: TextStyle(color: Colors.white.withValues(alpha: 0.8)),
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.white10),
|
||||||
),
|
),
|
||||||
],
|
child: ClipRRect(
|
||||||
),
|
borderRadius: BorderRadius.circular(12),
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
_viewportSize = Size(constraints.maxWidth, constraints.maxHeight);
|
|
||||||
|
|
||||||
// Taille du carré de crop (90% de la plus petite dimension)
|
|
||||||
_cropSize =
|
|
||||||
math.min(constraints.maxWidth, constraints.maxHeight) * 0.85;
|
|
||||||
|
|
||||||
// Calculer l'échelle initiale si pas encore fait
|
|
||||||
if (_scale == 1.0 && _offset == Offset.zero) {
|
|
||||||
_initializeImagePosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onScaleStart: _onScaleStart,
|
|
||||||
onScaleUpdate: _onScaleUpdate,
|
|
||||||
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(
|
child: Image.file(
|
||||||
File(widget.imagePath),
|
File(widget.imagePath),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
width: _viewportSize.width,
|
|
||||||
height: _viewportSize.height,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Overlay de recadrage
|
// Bouton Modifier
|
||||||
Positioned.fill(
|
Padding(
|
||||||
child: IgnorePointer(
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
child: CropOverlay(cropSize: _cropSize, showGrid: true),
|
child: ElevatedButton.icon(
|
||||||
),
|
onPressed: () {
|
||||||
),
|
// Logique de modification à venir
|
||||||
|
|
||||||
// Instructions en bas
|
|
||||||
Positioned(
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 24,
|
|
||||||
child: _buildInstructions(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
icon: const Icon(Icons.crop_rotate),
|
||||||
}
|
label: const Text('Modifier'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
void _initializeImagePosition() {
|
backgroundColor: const Color(0xFF1A73E8),
|
||||||
if (_imageSize == null) return;
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||||
final imageAspect = _imageSize!.width / _imageSize!.height;
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Boutons du bas
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 30),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Expanded(
|
||||||
Icons.touch_app,
|
child: OutlinedButton(
|
||||||
color: Colors.white.withValues(alpha: 0.8),
|
onPressed: () => Navigator.pop(context),
|
||||||
size: 20,
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: const BorderSide(color: Colors.white24),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
child: const Text('Recommencer', style: TextStyle(color: Colors.white)),
|
||||||
Text(
|
|
||||||
'Déplacez et zoomez pour cadrer la cible',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white.withValues(alpha: 0.9),
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(width: 16),
|
||||||
),
|
Expanded(
|
||||||
);
|
child: ElevatedButton(
|
||||||
}
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
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,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => AnalysisScreen(
|
builder: (_) => AnalysisScreen(
|
||||||
imagePath: croppedPath,
|
imagePath: widget.imagePath,
|
||||||
targetType: widget.targetType,
|
targetType: widget.targetType,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
},
|
||||||
if (!mounted) return;
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF1A73E8),
|
||||||
setState(() {
|
foregroundColor: Colors.white,
|
||||||
_isLoading = false;
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
});
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
child: const Text('Valider', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
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)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,56 +122,87 @@ 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(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: _sessions.isEmpty
|
: _sessions.isEmpty
|
||||||
? _buildEmptyState()
|
? _buildEmptyState()
|
||||||
: _buildContent(),
|
: _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(
|
|
||||||
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.history, size: 64, color: Colors.grey[400]),
|
Icon(Icons.history, size: 64, color: Colors.grey[400]),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
const Text('Aucune session sur cette période'),
|
||||||
'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,37 +211,17 @@ 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),
|
||||||
@@ -169,9 +231,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
// Single session mode
|
|
||||||
_statistics = _statisticsService.calculateStatistics(
|
|
||||||
[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>();
|
final repository = context.read<SessionRepository>();
|
||||||
_allSessions = await repository.getAllSessions();
|
_allSessions = await repository.getAllSessions();
|
||||||
_calculateStats();
|
_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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEmptyState() {
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
// 1. FILTRES (Arme et Distance)
|
||||||
Icons.analytics_outlined,
|
Row(
|
||||||
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: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _StatCard(
|
child: _buildDropdown('Arme utilisée', _selectedWeapon),
|
||||||
icon: Icons.gps_fixed,
|
),
|
||||||
title: 'Tirs',
|
const SizedBox(width: 12),
|
||||||
value: '${_statistics!.totalShots}',
|
Expanded(
|
||||||
color: AppTheme.primaryColor,
|
child: _buildDropdown('Distance', _selectedDistance),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// 2. DONNÉES RAPIDES (Tirs et Sessions)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildQuickStat(
|
||||||
|
'Nbre de tirs',
|
||||||
|
'${_statistics?.totalShots ?? 0}',
|
||||||
|
Icons.gps_fixed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _StatCard(
|
child: _buildQuickStat(
|
||||||
icon: Icons.score,
|
'Nbre de sessions',
|
||||||
title: 'Score Total',
|
'${_statistics?.sessions.length ?? 0}',
|
||||||
value: '${_statistics!.totalScore}',
|
Icons.analytics,
|
||||||
color: AppTheme.secondaryColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
}
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
Widget _buildHeatMapSection() {
|
// 3. LES GRAPHIQUES
|
||||||
return Card(
|
_buildChartSection(
|
||||||
child: Padding(
|
'Score',
|
||||||
padding: const EdgeInsets.all(16),
|
'${_statistics?.totalScore ?? 0}',
|
||||||
child: Column(
|
[8, 12, 10, 15, 14, 18],
|
||||||
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: 20),
|
||||||
|
_buildChartSection(
|
||||||
|
'Précision',
|
||||||
|
'${_statistics?.precision.precisionScore.toStringAsFixed(1)}%',
|
||||||
|
[60, 75, 70, 85, 80, 95],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 20),
|
||||||
Text(
|
_buildChartSection('Groupement moyen', '14.2 mm', [
|
||||||
'Repartition de vos tirs sur la cible',
|
20,
|
||||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
|
18,
|
||||||
),
|
22,
|
||||||
const SizedBox(height: 16),
|
15,
|
||||||
Center(
|
14,
|
||||||
child: HeatMapWidget(
|
12,
|
||||||
heatMap: _statistics!.heatMap,
|
]), // mm fictif pour l'exemple
|
||||||
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() {
|
const SizedBox(height: 30),
|
||||||
final precision = _statistics!.precision;
|
|
||||||
|
|
||||||
return Card(
|
// 4. BOUTON ACCUEIL
|
||||||
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(
|
SizedBox(
|
||||||
width: 80,
|
width: double.infinity,
|
||||||
height: 80,
|
height: 50,
|
||||||
child: CircularProgressIndicator(
|
child: ElevatedButton(
|
||||||
value: value / 100,
|
style: ElevatedButton.styleFrom(
|
||||||
strokeWidth: 8,
|
backgroundColor: const Color(0xFF1A73E8),
|
||||||
backgroundColor: Colors.grey.shade200,
|
shape: RoundedRectangleBorder(
|
||||||
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),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.compass_calibration,
|
|
||||||
color: AppTheme.primaryColor,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
onPressed: () => Navigator.of(
|
||||||
Expanded(
|
context,
|
||||||
child: Column(
|
).popUntil((route) => route.isFirst),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: const Text(
|
||||||
children: [
|
'ACCUEIL',
|
||||||
const Text('Direction dominante'),
|
|
||||||
Text(
|
|
||||||
regional.dominantDirection,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 18,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(label),
|
|
||||||
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSectorChip(String sector, int count, double percentage) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: count > 0
|
|
||||||
? AppTheme.primaryColor.withValues(alpha: 0.1)
|
|
||||||
: Colors.grey.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(
|
|
||||||
color: count > 0 ? AppTheme.primaryColor : Colors.grey.shade300,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'$sector: $count (${percentage.toStringAsFixed(0)}%)',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
color: Colors.white,
|
||||||
color: count > 0 ? AppTheme.primaryColor : Colors.grey.shade600,
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQuadrantGrid(Map<String, int> quadrants) {
|
// Widget pour les Dropdowns de filtres
|
||||||
return Table(
|
Widget _buildDropdown(String label, String value) {
|
||||||
border: TableBorder.all(color: Colors.grey.shade300),
|
|
||||||
children: [
|
|
||||||
TableRow(
|
|
||||||
children: [
|
|
||||||
_buildQuadrantCell('Haut-Gauche', quadrants['Haut-Gauche'] ?? 0),
|
|
||||||
_buildQuadrantCell('Haut-Droite', quadrants['Haut-Droite'] ?? 0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
TableRow(
|
|
||||||
children: [
|
|
||||||
_buildQuadrantCell('Bas-Gauche', quadrants['Bas-Gauche'] ?? 0),
|
|
||||||
_buildQuadrantCell('Bas-Droite', quadrants['Bas-Droite'] ?? 0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
color: Color.lerp(Colors.white, AppTheme.primaryColor, intensity * 0.5),
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF1E1E1E),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.white12),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
'$count',
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${percentage.toStringAsFixed(0)}%',
|
|
||||||
style: TextStyle(color: Colors.grey.shade600),
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(fontSize: 10),
|
style: const TextStyle(color: Colors.white54, fontSize: 10),
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
|
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) {},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getBiasDescription(double biasX, double biasY) {
|
// Widget pour les petites cartes de stats
|
||||||
final descriptions = <String>[];
|
Widget _buildQuickStat(String label, String value, IconData icon) {
|
||||||
|
return Container(
|
||||||
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),
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF1E1E1E),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: color, size: 32),
|
Icon(icon, color: const Color(0xFF1A73E8), size: 20),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
color: Colors.white,
|
||||||
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: color,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(title, style: TextStyle(color: Colors.grey.shade600)),
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(color: Colors.white54, fontSize: 12),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widget pour une section de graphique
|
||||||
|
Widget _buildChartSection(
|
||||||
|
String title,
|
||||||
|
String value,
|
||||||
|
List<double> dataPoints,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF1E1E1E),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import 'services/target_detection_service.dart';
|
|||||||
import 'services/score_calculator_service.dart';
|
import 'services/score_calculator_service.dart';
|
||||||
import 'services/grouping_analyzer_service.dart';
|
import 'services/grouping_analyzer_service.dart';
|
||||||
import 'services/image_processing_service.dart';
|
import 'services/image_processing_service.dart';
|
||||||
import 'services/yolo_impact_detection_service.dart';
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -34,13 +33,9 @@ void main() async {
|
|||||||
Provider<ImageProcessingService>(
|
Provider<ImageProcessingService>(
|
||||||
create: (_) => ImageProcessingService(),
|
create: (_) => ImageProcessingService(),
|
||||||
),
|
),
|
||||||
Provider<YOLOImpactDetectionService>(
|
|
||||||
create: (_) => YOLOImpactDetectionService(),
|
|
||||||
),
|
|
||||||
Provider<TargetDetectionService>(
|
Provider<TargetDetectionService>(
|
||||||
create: (context) => TargetDetectionService(
|
create: (context) => TargetDetectionService(
|
||||||
imageProcessingService: context.read<ImageProcessingService>(),
|
imageProcessingService: context.read<ImageProcessingService>(),
|
||||||
yoloService: context.read<YOLOImpactDetectionService>(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Provider<ScoreCalculatorService>(
|
Provider<ScoreCalculatorService>(
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import 'dart:math' as math;
|
|||||||
import '../data/models/target_type.dart';
|
import '../data/models/target_type.dart';
|
||||||
import 'image_processing_service.dart';
|
import 'image_processing_service.dart';
|
||||||
import 'opencv_impact_detection_service.dart';
|
import 'opencv_impact_detection_service.dart';
|
||||||
import 'yolo_impact_detection_service.dart';
|
|
||||||
|
|
||||||
export 'image_processing_service.dart'
|
export 'image_processing_service.dart'
|
||||||
show ImpactDetectionSettings, ReferenceImpact, ImpactCharacteristics;
|
show ImpactDetectionSettings, ReferenceImpact, ImpactCharacteristics;
|
||||||
export 'opencv_impact_detection_service.dart'
|
export 'opencv_impact_detection_service.dart'
|
||||||
@@ -55,16 +53,13 @@ class DetectedImpactResult {
|
|||||||
class TargetDetectionService {
|
class TargetDetectionService {
|
||||||
final ImageProcessingService _imageProcessingService;
|
final ImageProcessingService _imageProcessingService;
|
||||||
final OpenCVImpactDetectionService _opencvService;
|
final OpenCVImpactDetectionService _opencvService;
|
||||||
final YOLOImpactDetectionService _yoloService;
|
|
||||||
|
|
||||||
TargetDetectionService({
|
TargetDetectionService({
|
||||||
ImageProcessingService? imageProcessingService,
|
ImageProcessingService? imageProcessingService,
|
||||||
OpenCVImpactDetectionService? opencvService,
|
OpenCVImpactDetectionService? opencvService,
|
||||||
YOLOImpactDetectionService? yoloService,
|
|
||||||
}) : _imageProcessingService =
|
}) : _imageProcessingService =
|
||||||
imageProcessingService ?? ImageProcessingService(),
|
imageProcessingService ?? ImageProcessingService(),
|
||||||
_opencvService = opencvService ?? OpenCVImpactDetectionService(),
|
_opencvService = opencvService ?? OpenCVImpactDetectionService();
|
||||||
_yoloService = yoloService ?? YOLOImpactDetectionService();
|
|
||||||
|
|
||||||
/// Detect target and impacts from an image file
|
/// Detect target and impacts from an image file
|
||||||
TargetDetectionResult detectTarget(String imagePath, TargetType targetType) {
|
TargetDetectionResult detectTarget(String imagePath, TargetType targetType) {
|
||||||
@@ -379,40 +374,4 @@ class TargetDetectionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Détecte les impacts en utilisant YOLOv8
|
|
||||||
Future<List<DetectedImpactResult>> detectImpactsWithYOLO(
|
|
||||||
String imagePath,
|
|
||||||
TargetType targetType,
|
|
||||||
double centerX,
|
|
||||||
double centerY,
|
|
||||||
double radius,
|
|
||||||
int ringCount,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
final impacts = await _yoloService.detectImpacts(imagePath);
|
|
||||||
|
|
||||||
return impacts.map((impact) {
|
|
||||||
final score = targetType == TargetType.concentric
|
|
||||||
? _calculateConcentricScoreWithRings(
|
|
||||||
impact.x,
|
|
||||||
impact.y,
|
|
||||||
centerX,
|
|
||||||
centerY,
|
|
||||||
radius,
|
|
||||||
ringCount,
|
|
||||||
)
|
|
||||||
: _calculateSilhouetteScore(impact.x, impact.y, centerX, centerY);
|
|
||||||
|
|
||||||
return DetectedImpactResult(
|
|
||||||
x: impact.x,
|
|
||||||
y: impact.y,
|
|
||||||
radius: impact.radius,
|
|
||||||
suggestedScore: score,
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
} catch (e) {
|
|
||||||
print('Erreur détection YOLOv8: $e');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:math' as math;
|
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:tflite_flutter/tflite_flutter.dart';
|
|
||||||
import 'package:image/image.dart' as img;
|
|
||||||
import 'target_detection_service.dart';
|
|
||||||
|
|
||||||
class YOLOImpactDetectionService {
|
|
||||||
Interpreter? _interpreter;
|
|
||||||
|
|
||||||
static const String modelPath = 'assets/models/yolov11n_impact.tflite';
|
|
||||||
static const String labelsPath = 'assets/models/labels.txt';
|
|
||||||
|
|
||||||
Future<void> init() async {
|
|
||||||
if (_interpreter != null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Try loading the specific YOLOv11 model first, fallback to v8 if not found
|
|
||||||
try {
|
|
||||||
_interpreter = await Interpreter.fromAsset(modelPath);
|
|
||||||
} catch (e) {
|
|
||||||
print('YOLOv11 model not found at $modelPath, trying YOLOv8 fallback');
|
|
||||||
_interpreter = await Interpreter.fromAsset(
|
|
||||||
'assets/models/yolov8n_impact.tflite',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
print('YOLO Interpreter loaded successfully');
|
|
||||||
} catch (e) {
|
|
||||||
print('Error loading YOLO model: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<DetectedImpactResult>> detectImpacts(String imagePath) async {
|
|
||||||
if (_interpreter == null) await init();
|
|
||||||
if (_interpreter == null) return [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
final bytes = File(imagePath).readAsBytesSync();
|
|
||||||
final originalImage = img.decodeImage(bytes);
|
|
||||||
if (originalImage == null) return [];
|
|
||||||
|
|
||||||
// YOLOv8/v11 usually takes 640x640
|
|
||||||
const int inputSize = 640;
|
|
||||||
final resizedImage = img.copyResize(
|
|
||||||
originalImage,
|
|
||||||
width: inputSize,
|
|
||||||
height: inputSize,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prepare input tensor
|
|
||||||
var input = _imageToByteListFloat32(resizedImage, inputSize);
|
|
||||||
|
|
||||||
// Raw YOLO output shape usually [1, 4 + num_classes, 8400]
|
|
||||||
// For single class "impact", it's [1, 5, 8400]
|
|
||||||
var output = List<double>.filled(1 * 5 * 8400, 0).reshape([1, 5, 8400]);
|
|
||||||
|
|
||||||
_interpreter!.run(input, output);
|
|
||||||
|
|
||||||
return _processOutput(
|
|
||||||
output[0],
|
|
||||||
originalImage.width,
|
|
||||||
originalImage.height,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
print('Error during YOLO inference: $e');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DetectedImpactResult> _processOutput(
|
|
||||||
List<List<double>> output,
|
|
||||||
int imgWidth,
|
|
||||||
int imgHeight,
|
|
||||||
) {
|
|
||||||
final List<_Detection> candidates = [];
|
|
||||||
const double threshold = 0.25;
|
|
||||||
|
|
||||||
// output is [5, 8400] -> [x, y, w, h, conf]
|
|
||||||
for (int i = 0; i < 8400; i++) {
|
|
||||||
final double confidence = output[4][i];
|
|
||||||
if (confidence > threshold) {
|
|
||||||
candidates.add(
|
|
||||||
_Detection(
|
|
||||||
x: output[0][i],
|
|
||||||
y: output[1][i],
|
|
||||||
w: output[2][i],
|
|
||||||
h: output[3][i],
|
|
||||||
confidence: confidence,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply Non-Max Suppression (NMS)
|
|
||||||
final List<_Detection> suppressed = _nms(candidates);
|
|
||||||
|
|
||||||
return suppressed
|
|
||||||
.map(
|
|
||||||
(det) => DetectedImpactResult(
|
|
||||||
x: det.x / 640.0,
|
|
||||||
y: det.y / 640.0,
|
|
||||||
radius: 5.0,
|
|
||||||
suggestedScore: 0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<_Detection> _nms(List<_Detection> detections) {
|
|
||||||
if (detections.isEmpty) return [];
|
|
||||||
|
|
||||||
// Sort by confidence descending
|
|
||||||
detections.sort((a, b) => b.confidence.compareTo(a.confidence));
|
|
||||||
|
|
||||||
final List<_Detection> selected = [];
|
|
||||||
final List<bool> active = List.filled(detections.length, true);
|
|
||||||
|
|
||||||
for (int i = 0; i < detections.length; i++) {
|
|
||||||
if (!active[i]) continue;
|
|
||||||
|
|
||||||
selected.add(detections[i]);
|
|
||||||
|
|
||||||
for (int j = i + 1; j < detections.length; j++) {
|
|
||||||
if (!active[j]) continue;
|
|
||||||
|
|
||||||
if (_iou(detections[i], detections[j]) > 0.45) {
|
|
||||||
active[j] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _iou(_Detection a, _Detection b) {
|
|
||||||
final double areaA = a.w * a.h;
|
|
||||||
final double areaB = b.w * b.h;
|
|
||||||
|
|
||||||
final double x1 = math.max(a.x - a.w / 2, b.x - b.w / 2);
|
|
||||||
final double y1 = math.max(a.y - a.h / 2, b.y - b.h / 2);
|
|
||||||
final double x2 = math.min(a.x + a.w / 2, b.x + b.w / 2);
|
|
||||||
final double y2 = math.min(a.y + a.h / 2, b.y + b.h / 2);
|
|
||||||
|
|
||||||
final double intersection = math.max(0.0, x2 - x1) * math.max(0.0, y2 - y1);
|
|
||||||
return intersection / (areaA + areaB - intersection);
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint8List _imageToByteListFloat32(img.Image image, int inputSize) {
|
|
||||||
var convertedBytes = Float32List(1 * inputSize * inputSize * 3);
|
|
||||||
var buffer = Float32List.view(convertedBytes.buffer);
|
|
||||||
int pixelIndex = 0;
|
|
||||||
for (int i = 0; i < inputSize; i++) {
|
|
||||||
for (int j = 0; j < inputSize; j++) {
|
|
||||||
var pixel = image.getPixel(j, i);
|
|
||||||
buffer[pixelIndex++] = (pixel.r / 255.0);
|
|
||||||
buffer[pixelIndex++] = (pixel.g / 255.0);
|
|
||||||
buffer[pixelIndex++] = (pixel.b / 255.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return convertedBytes.buffer.asUint8List();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Detection {
|
|
||||||
final double x, y, w, h, confidence;
|
|
||||||
_Detection({
|
|
||||||
required this.x,
|
|
||||||
required this.y,
|
|
||||||
required this.w,
|
|
||||||
required this.h,
|
|
||||||
required this.confidence,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
tflite_flutter
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
143
pubspec.lock
143
pubspec.lock
@@ -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:
|
||||||
@@ -536,14 +613,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
quiver:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: quiver
|
|
||||||
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.2.2"
|
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -657,18 +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"
|
||||||
tflite_flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: tflite_flutter
|
|
||||||
sha256: ffb8651fdb116ab0131d6dc47ff73883e0f634ad1ab12bb2852eef1bbeab4a6a
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.10.4"
|
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -709,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:
|
||||||
@@ -734,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"
|
||||||
|
|||||||
13
pubspec.yaml
13
pubspec.yaml
@@ -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,16 +58,18 @@ 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.10.4
|
# tflite_flutter: ^0.11.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -91,6 +95,7 @@ flutter:
|
|||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
# assets:
|
||||||
|
# - assets/models/yolov8n_32.tflite
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
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
|
||||||
tflite_flutter
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|||||||
Reference in New Issue
Block a user