11 Commits

70 changed files with 5547 additions and 3349 deletions

12
.gitignore vendored
View File

@@ -43,3 +43,15 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
# Backendia
backendia/node_modules/
backendia/dashboard/node_modules/
backendia/dashboard/.next/
backendia/dashboard/out/
backendia/exports/*.zip
backendia/uploads/images/*
!backendia/uploads/images/.gitkeep
backendia/uploads/data/*
!backendia/uploads/data/.gitkeep
!backendia/uploads/data/database.sqlite

BIN
analysis_report.txt Normal file

Binary file not shown.

View File

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

6
backendia/dashboard/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -0,0 +1,116 @@
{
"session_id": "session_1777452202935",
"timestamp": "2026-04-29T08:43:22.943303",
"device_info": {
"model": "google sdk_gphone64_x86_64",
"os": "Android 16"
},
"target_metadata": {
"type": "concentric",
"distance_meters": 25,
"weapon": "Unknown"
},
"plotting": {
"target_corners": [
{
"norm_x": 0.09999999999999998,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.9
},
{
"norm_x": 0.09999999999999998,
"norm_y": 0.9
}
],
"impacts": [
{
"id": 1,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.3934941044560185,
"norm_y": 0.665676540798611
}
},
{
"id": 2,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.6055302372685185,
"norm_y": 0.6953124999999999
}
},
{
"id": 3,
"label": "bullet_hole",
"score": 8,
"coords": {
"norm_x": 0.5981137876157407,
"norm_y": 0.5249565972222221
}
},
{
"id": 4,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.6759033203125,
"norm_y": 0.4786376953125
}
},
{
"id": 5,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.48608398437499994,
"norm_y": 0.5490315755208333
}
},
{
"id": 6,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.5101625795717593,
"norm_y": 0.43238661024305547
}
},
{
"id": 7,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.6296079282407407,
"norm_y": 0.4203152126736111
}
},
{
"id": 8,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.46386718749999994,
"norm_y": 0.48046875
}
},
{
"id": 9,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.3379521122685185,
"norm_y": 0.4397786458333333
}
}
]
}
}

View File

@@ -0,0 +1,116 @@
{
"session_id": "session_1777452481252",
"timestamp": "2026-04-29T08:48:01.259705",
"device_info": {
"model": "google sdk_gphone64_x86_64",
"os": "Android 16"
},
"target_metadata": {
"type": "concentric",
"distance_meters": 25,
"weapon": "Unknown"
},
"plotting": {
"target_corners": [
{
"norm_x": 0.09999999999999998,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.9
},
{
"norm_x": 0.09999999999999998,
"norm_y": 0.9
}
],
"impacts": [
{
"id": 1,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.3934941044560185,
"norm_y": 0.665676540798611
}
},
{
"id": 2,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.6055302372685185,
"norm_y": 0.6953124999999999
}
},
{
"id": 3,
"label": "bullet_hole",
"score": 8,
"coords": {
"norm_x": 0.5981137876157407,
"norm_y": 0.5249565972222221
}
},
{
"id": 4,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.6759033203125,
"norm_y": 0.4786376953125
}
},
{
"id": 5,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.48608398437499994,
"norm_y": 0.5490315755208333
}
},
{
"id": 6,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.5101625795717593,
"norm_y": 0.43238661024305547
}
},
{
"id": 7,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.6296079282407407,
"norm_y": 0.4203152126736111
}
},
{
"id": 8,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.46386718749999994,
"norm_y": 0.48046875
}
},
{
"id": 9,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.3379521122685185,
"norm_y": 0.4397786458333333
}
}
]
}
}

View File

@@ -0,0 +1,44 @@
{
"session_id": "session_1777452613625",
"timestamp": "2026-04-29T08:50:13.631206",
"device_info": {
"model": "google sdk_gphone64_x86_64",
"os": "Android 16"
},
"target_metadata": {
"type": "concentric",
"distance_meters": 25,
"weapon": "Unknown"
},
"plotting": {
"target_corners": [
{
"norm_x": 0.09999999999999998,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.9
},
{
"norm_x": 0.09999999999999998,
"norm_y": 0.9
}
],
"impacts": [
{
"id": 1,
"label": "bullet_hole",
"score": 10,
"coords": {
"norm_x": 0.49810836226851846,
"norm_y": 0.4981011284722222
}
}
]
}
}

View File

@@ -0,0 +1,144 @@
{
"session_id": "session_1777463308809",
"wallet_hash": "cc60fcaa5ef7fdc9be3dd1adba4157fa6dfae2845da45b830b5ef2b7f473d887",
"timestamp": "2026-04-29T11:48:28.834236",
"device_info": {
"model": "google sdk_gphone64_x86_64",
"os": "Android 16"
},
"target_metadata": {
"type": "concentric",
"distance_meters": 25,
"weapon": "Unknown"
},
"plotting": {
"target_corners": [
{
"norm_x": 0.09999999999999998,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.9
},
{
"norm_x": 0.09999999999999998,
"norm_y": 0.9
}
],
"impacts": [
{
"id": 1,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.38424660011574074,
"norm_y": 0.3666042751736111
}
},
{
"id": 2,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.6055302372685185,
"norm_y": 0.396240234375
}
},
{
"id": 3,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.5860903139467593,
"norm_y": 0.5999620225694443
}
},
{
"id": 4,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.33517433449074074,
"norm_y": 0.5592041015625
}
},
{
"id": 5,
"label": "bullet_hole",
"score": 8,
"coords": {
"norm_x": 0.6073612919560185,
"norm_y": 0.5101047092013888
}
},
{
"id": 6,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.432373046875,
"norm_y": 0.46663411458333337
}
},
{
"id": 7,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.45181297019675926,
"norm_y": 0.5564236111111112
}
},
{
"id": 8,
"label": "bullet_hole",
"score": 10,
"coords": {
"norm_x": 0.5221860532407407,
"norm_y": 0.48792860243055547
}
},
{
"id": 9,
"label": "bullet_hole",
"score": 3,
"coords": {
"norm_x": 0.6564335575810184,
"norm_y": 0.7536349826388888
}
},
{
"id": 10,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.4712528935185185,
"norm_y": 0.7018229166666666
}
},
{
"id": 11,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.3981327763310185,
"norm_y": 0.7018229166666666
}
},
{
"id": 12,
"label": "bullet_hole",
"score": 2,
"coords": {
"norm_x": 0.48144531249999994,
"norm_y": 0.8212483723958334
}
}
]
}
}

View File

@@ -0,0 +1,117 @@
{
"session_id": "session_1777463703756",
"wallet_hash": "cc60fcaa5ef7fdc9be3dd1adba4157fa6dfae2845da45b830b5ef2b7f473d887",
"timestamp": "2026-04-29T11:55:03.765285",
"device_info": {
"model": "google sdk_gphone64_x86_64",
"os": "Android 16"
},
"target_metadata": {
"type": "concentric",
"distance_meters": 25,
"weapon": "Unknown"
},
"plotting": {
"target_corners": [
{
"norm_x": 0.09999999999999998,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.9
},
{
"norm_x": 0.09999999999999998,
"norm_y": 0.9
}
],
"impacts": [
{
"id": 1,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.38424660011574074,
"norm_y": 0.45178222656249994
}
},
{
"id": 2,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.6518247251157407,
"norm_y": 0.3638237847222222
}
},
{
"id": 3,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.5490415219907407,
"norm_y": 0.66845703125
}
},
{
"id": 4,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.405517578125,
"norm_y": 0.6758490668402777
}
},
{
"id": 5,
"label": "bullet_hole",
"score": 3,
"coords": {
"norm_x": 0.7268066406249999,
"norm_y": 0.6758490668402777
}
},
{
"id": 6,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.5471806278935185,
"norm_y": 0.529568142361111
}
},
{
"id": 7,
"label": "bullet_hole",
"score": 8,
"coords": {
"norm_x": 0.44720504195601846,
"norm_y": 0.5980631510416666
}
},
{
"id": 8,
"label": "bullet_hole",
"score": 8,
"coords": {
"norm_x": 0.45458984375,
"norm_y": 0.4221462673611111
}
},
{
"id": 9,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.5203251591435185,
"norm_y": 0.3638237847222222
}
}
]
}
}

View File

@@ -0,0 +1,117 @@
{
"session_id": "session_1777464156234",
"wallet_hash": "cc60fcaa5ef7fdc9be3dd1adba4157fa6dfae2845da45b830b5ef2b7f473d887",
"timestamp": "2026-04-29T12:02:36.255849",
"device_info": {
"model": "google sdk_gphone64_x86_64",
"os": "Android 16"
},
"target_metadata": {
"type": "concentric",
"distance_meters": 25,
"weapon": "Unknown"
},
"plotting": {
"target_corners": [
{
"norm_x": 0.09999999999999998,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.9
},
{
"norm_x": 0.09999999999999998,
"norm_y": 0.9
}
],
"impacts": [
{
"id": 1,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.38424660011574074,
"norm_y": 0.45178222656249994
}
},
{
"id": 2,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.6518247251157407,
"norm_y": 0.3638237847222222
}
},
{
"id": 3,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.5490415219907407,
"norm_y": 0.66845703125
}
},
{
"id": 4,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.405517578125,
"norm_y": 0.6758490668402777
}
},
{
"id": 5,
"label": "bullet_hole",
"score": 3,
"coords": {
"norm_x": 0.7268066406249999,
"norm_y": 0.6758490668402777
}
},
{
"id": 6,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.5471806278935185,
"norm_y": 0.529568142361111
}
},
{
"id": 7,
"label": "bullet_hole",
"score": 8,
"coords": {
"norm_x": 0.44720504195601846,
"norm_y": 0.5980631510416666
}
},
{
"id": 8,
"label": "bullet_hole",
"score": 8,
"coords": {
"norm_x": 0.45458984375,
"norm_y": 0.4221462673611111
}
},
{
"id": 9,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.5203251591435185,
"norm_y": 0.3638237847222222
}
}
]
}
}

View File

@@ -0,0 +1,180 @@
{
"session_id": "session_1777464607256",
"wallet_hash": "cc60fcaa5ef7fdc9be3dd1adba4157fa6dfae2845da45b830b5ef2b7f473d887",
"timestamp": "2026-04-29T12:10:07.260954",
"device_info": {
"model": "google sdk_gphone64_x86_64",
"os": "Android 16"
},
"target_metadata": {
"type": "concentric",
"distance_meters": 25,
"weapon": "Unknown"
},
"plotting": {
"target_corners": [
{
"norm_x": 0.09999999999999998,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.9
},
{
"norm_x": 0.09999999999999998,
"norm_y": 0.9
}
],
"impacts": [
{
"id": 1,
"label": "bullet_hole",
"score": 3,
"coords": {
"norm_x": 0.31570457175925926,
"norm_y": 0.7388509114583333
}
},
{
"id": 2,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.3184814453125,
"norm_y": 0.6489935980902778
}
},
{
"id": 3,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.3110966435185185,
"norm_y": 0.529568142361111
}
},
{
"id": 4,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.31570457175925926,
"norm_y": 0.4416097005208333
}
},
{
"id": 5,
"label": "bullet_hole",
"score": 4,
"coords": {
"norm_x": 0.31570457175925926,
"norm_y": 0.34252929687499994
}
},
{
"id": 6,
"label": "bullet_hole",
"score": 3,
"coords": {
"norm_x": 0.32589699074074074,
"norm_y": 0.2332763671875
}
},
{
"id": 7,
"label": "bullet_hole",
"score": 3,
"coords": {
"norm_x": 0.45458984375,
"norm_y": 0.19624837239583331
}
},
{
"id": 8,
"label": "bullet_hole",
"score": 3,
"coords": {
"norm_x": 0.5981137876157407,
"norm_y": 0.23049587673611108
}
},
{
"id": 9,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.6907045717592593,
"norm_y": 0.3546006944444444
}
},
{
"id": 10,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.699951171875,
"norm_y": 0.48046875
}
},
{
"id": 11,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.6268310546874999,
"norm_y": 0.5416395399305556
}
},
{
"id": 12,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.49072265624999994,
"norm_y": 0.5610351562499999
}
},
{
"id": 13,
"label": "bullet_hole",
"score": 8,
"coords": {
"norm_x": 0.4203495732060185,
"norm_y": 0.536960177951389
}
},
{
"id": 14,
"label": "bullet_hole",
"score": 6,
"coords": {
"norm_x": 0.5370180483217593,
"norm_y": 0.6879204644097222
}
},
{
"id": 15,
"label": "bullet_hole",
"score": 3,
"coords": {
"norm_x": 0.6175537109375,
"norm_y": 0.7601453993055556
}
},
{
"id": 16,
"label": "bullet_hole",
"score": 1,
"coords": {
"norm_x": 0.7027280454282406,
"norm_y": 0.8406439887152777
}
}
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -0,0 +1 @@
# Keep directory structure

View File

@@ -0,0 +1,270 @@
{
"session_id": "session_1777553535029",
"wallet_hash": "cc60fcaa5ef7fdc9be3dd1adba4157fa6dfae2845da45b830b5ef2b7f473d887",
"timestamp": "2026-04-30T12:52:15.048830",
"device_info": {
"model": "google sdk_gphone64_x86_64",
"os": "Android 16"
},
"target_metadata": {
"type": "concentric",
"distance_meters": 25,
"weapon": "Unknown"
},
"plotting": {
"target_corners": [
{
"norm_x": 0.09999999999999998,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.9
},
{
"norm_x": 0.09999999999999998,
"norm_y": 0.9
}
],
"impacts": [
{
"id": 1,
"label": "bullet_hole",
"score": 0,
"coords": {
"norm_x": 0.17681929976851848,
"norm_y": 0.8851318359375
}
},
{
"id": 2,
"label": "bullet_hole",
"score": 0,
"coords": {
"norm_x": 0.17681929976851848,
"norm_y": 0.7721489800347222
}
},
{
"id": 3,
"label": "bullet_hole",
"score": 1,
"coords": {
"norm_x": 0.17959617332175923,
"norm_y": 0.6888020833333334
}
},
{
"id": 4,
"label": "bullet_hole",
"score": 2,
"coords": {
"norm_x": 0.17959617332175923,
"norm_y": 0.6342095269097222
}
},
{
"id": 5,
"label": "bullet_hole",
"score": 2,
"coords": {
"norm_x": 0.17959617332175923,
"norm_y": 0.560153537326389
}
},
{
"id": 6,
"label": "bullet_hole",
"score": 2,
"coords": {
"norm_x": 0.17959617332175923,
"norm_y": 0.5138346354166666
}
},
{
"id": 7,
"label": "bullet_hole",
"score": 2,
"coords": {
"norm_x": 0.1814579716435185,
"norm_y": 0.45273166232638884
}
},
{
"id": 8,
"label": "bullet_hole",
"score": 2,
"coords": {
"norm_x": 0.18701171875,
"norm_y": 0.3878987630208333
}
},
{
"id": 9,
"label": "bullet_hole",
"score": 1,
"coords": {
"norm_x": 0.18887351707175926,
"norm_y": 0.2925482855902778
}
},
{
"id": 10,
"label": "bullet_hole",
"score": 0,
"coords": {
"norm_x": 0.2036747685185185,
"norm_y": 0.20085991753472218
}
},
{
"id": 11,
"label": "bullet_hole",
"score": 0,
"coords": {
"norm_x": 0.27035499855324074,
"norm_y": 0.16200086805555558
}
},
{
"id": 12,
"label": "bullet_hole",
"score": 1,
"coords": {
"norm_x": 0.37588523582175926,
"norm_y": 0.1546088324652778
}
},
{
"id": 13,
"label": "bullet_hole",
"score": 2,
"coords": {
"norm_x": 0.46292136863425926,
"norm_y": 0.1518283420138889
}
},
{
"id": 14,
"label": "bullet_hole",
"score": 1,
"coords": {
"norm_x": 0.6814579716435185,
"norm_y": 0.1499294704861111
}
},
{
"id": 15,
"label": "bullet_hole",
"score": 2,
"coords": {
"norm_x": 0.5509033203125,
"norm_y": 0.1546088324652778
}
},
{
"id": 16,
"label": "bullet_hole",
"score": 1,
"coords": {
"norm_x": 0.6156919126157407,
"norm_y": 0.1546088324652778
}
},
{
"id": 17,
"label": "bullet_hole",
"score": 1,
"coords": {
"norm_x": 0.7703251591435185,
"norm_y": 0.2360568576388889
}
},
{
"id": 18,
"label": "bullet_hole",
"score": 4,
"coords": {
"norm_x": 0.7110595703125,
"norm_y": 0.3536512586805555
}
},
{
"id": 19,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.6027524594907407,
"norm_y": 0.4129231770833333
}
},
{
"id": 20,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.4823911313657407,
"norm_y": 0.4304877387152777
}
},
{
"id": 21,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.39627097800925926,
"norm_y": 0.43509928385416663
}
},
{
"id": 22,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.289794921875,
"norm_y": 0.43238661024305547
}
},
{
"id": 23,
"label": "bullet_hole",
"score": 2,
"coords": {
"norm_x": 0.7166440610532407,
"norm_y": 0.7693684895833333
}
},
{
"id": 24,
"label": "bullet_hole",
"score": 3,
"coords": {
"norm_x": 0.7314453125,
"norm_y": 0.7008734809027778
}
},
{
"id": 25,
"label": "bullet_hole",
"score": 1,
"coords": {
"norm_x": 0.7879331235532407,
"norm_y": 0.7601453993055556
}
},
{
"id": 26,
"label": "bullet_hole",
"score": 0,
"coords": {
"norm_x": 0.736083984375,
"norm_y": 0.828640407986111
}
}
]
}
}

View File

@@ -0,0 +1 @@
# Keep directory structure

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,90 @@
{
"session_id": "session_1777554006515",
"wallet_hash": "cc60fcaa5ef7fdc9be3dd1adba4157fa6dfae2845da45b830b5ef2b7f473d887",
"timestamp": "2026-04-30T13:00:06.519693",
"device_info": {
"model": "google sdk_gphone64_x86_64",
"os": "Android 16"
},
"target_metadata": {
"type": "concentric",
"distance_meters": 25,
"weapon": "Unknown"
},
"plotting": {
"target_corners": [
{
"norm_x": 0.09999999999999998,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.09999999999999998
},
{
"norm_x": 0.9,
"norm_y": 0.9
},
{
"norm_x": 0.09999999999999998,
"norm_y": 0.9
}
],
"impacts": [
{
"id": 1,
"label": "bullet_hole",
"score": 5,
"coords": {
"norm_x": 0.267578125,
"norm_y": 0.5554741753472222
}
},
{
"id": 2,
"label": "bullet_hole",
"score": 7,
"coords": {
"norm_x": 0.5712583188657407,
"norm_y": 0.3592800564236111
}
},
{
"id": 3,
"label": "bullet_hole",
"score": 4,
"coords": {
"norm_x": 0.7555239076967593,
"norm_y": 0.5703938802083333
}
},
{
"id": 4,
"label": "bullet_hole",
"score": 4,
"coords": {
"norm_x": 0.6666259765625,
"norm_y": 0.6759168836805556
}
},
{
"id": 5,
"label": "bullet_hole",
"score": 9,
"coords": {
"norm_x": 0.5490415219907407,
"norm_y": 0.5212944878472222
}
},
{
"id": 6,
"label": "bullet_hole",
"score": 8,
"coords": {
"norm_x": 0.4897768373842592,
"norm_y": 0.39352756076388895
}
}
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

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

View File

@@ -1,19 +1,36 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'core/theme/theme_provider.dart';
import 'core/theme/app_theme.dart'; import 'core/theme/app_theme.dart';
import 'main_navigation_holder.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});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return Consumer<ThemeProvider>(
title: 'Bully - Analyse de Cibles', builder: (context, themeProvider, child) {
debugShowCheckedModeBanner: false, return MaterialApp(
theme: AppTheme.lightTheme, title: 'Bully - Analyse de Cibles',
darkTheme: AppTheme.darkTheme, debugShowCheckedModeBanner: false,
themeMode: ThemeMode.system, theme: AppTheme.lightTheme,
home: const HomeScreen(), darkTheme: AppTheme.darkTheme,
themeMode: themeProvider.themeMode,
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 MainNavigationHolder(),
);
},
); );
} }
} }

View File

@@ -3,11 +3,14 @@ class AppConstants {
// Database // Database
static const String databaseName = 'bully_targets.db'; static const String databaseName = 'bully_targets.db';
static const int databaseVersion = 1; static const int databaseVersion = 6;
// Tables // Tables
static const String sessionsTable = 'sessions'; static const String sessionsTable = 'sessions';
static const String targetAnalysesTable = 'target_analyses';
static const String shotsTable = 'shots'; static const String shotsTable = 'shots';
static const String weaponsTable = 'weapons';
static const String maintenanceTable = 'weapon_maintenance';
// Image processing // Image processing
static const double minImpactRadius = 2.0; static const double minImpactRadius = 2.0;

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ThemeProvider with ChangeNotifier {
static const String _themeModeKey = 'user_theme_mode';
ThemeMode _themeMode = ThemeMode.system;
ThemeMode get themeMode => _themeMode;
ThemeProvider() {
loadThemeMode();
}
Future<void> loadThemeMode() async {
final prefs = await SharedPreferences.getInstance();
final modeIndex = prefs.getInt(_themeModeKey);
if (modeIndex != null) {
_themeMode = ThemeMode.values[modeIndex];
notifyListeners();
}
}
Future<void> setThemeMode(ThemeMode mode) async {
if (_themeMode == mode) return;
_themeMode = mode;
notifyListeners();
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_themeModeKey, mode.index);
}
String get themeModeName {
switch (_themeMode) {
case ThemeMode.system:
return 'Automatique';
case ThemeMode.light:
return 'Clair';
case ThemeMode.dark:
return 'Sombre';
}
}
}

View File

@@ -1,7 +1,10 @@
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import '../models/session.dart'; import '../models/session.dart';
import '../models/target_analysis.dart';
import '../models/shot.dart'; import '../models/shot.dart';
import '../models/weapon.dart';
import '../models/maintenance.dart';
import '../../core/constants/app_constants.dart'; import '../../core/constants/app_constants.dart';
class DatabaseHelper { class DatabaseHelper {
@@ -36,6 +39,19 @@ class DatabaseHelper {
await db.execute(''' await db.execute('''
CREATE TABLE ${AppConstants.sessionsTable} ( CREATE TABLE ${AppConstants.sessionsTable} (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
weapon TEXT NOT NULL,
weapon_id TEXT,
max_shots_per_target INTEGER NOT NULL,
created_at TEXT NOT NULL,
notes TEXT,
distance INTEGER DEFAULT 25
)
''');
await db.execute('''
CREATE TABLE ${AppConstants.targetAnalysesTable} (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
target_type TEXT NOT NULL, target_type TEXT NOT NULL,
image_path TEXT NOT NULL, image_path TEXT NOT NULL,
total_score INTEGER NOT NULL, total_score INTEGER NOT NULL,
@@ -46,7 +62,8 @@ class DatabaseHelper {
notes TEXT, notes TEXT,
target_center_x REAL, target_center_x REAL,
target_center_y REAL, target_center_y REAL,
target_radius REAL target_radius REAL,
FOREIGN KEY (session_id) REFERENCES ${AppConstants.sessionsTable}(id) ON DELETE CASCADE
) )
'''); ''');
@@ -56,22 +73,212 @@ class DatabaseHelper {
x REAL NOT NULL, x REAL NOT NULL,
y REAL NOT NULL, y REAL NOT NULL,
score INTEGER NOT NULL, score INTEGER NOT NULL,
session_id TEXT NOT NULL, analysis_id TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES ${AppConstants.sessionsTable}(id) ON DELETE CASCADE FOREIGN KEY (analysis_id) REFERENCES ${AppConstants.targetAnalysesTable}(id) ON DELETE CASCADE
) )
'''); ''');
await db.execute(''' await db.execute('''
CREATE INDEX idx_shots_session_id ON ${AppConstants.shotsTable}(session_id) CREATE INDEX idx_target_analyses_session_id ON ${AppConstants.targetAnalysesTable}(session_id)
''');
await db.execute('''
CREATE INDEX idx_shots_analysis_id ON ${AppConstants.shotsTable}(analysis_id)
'''); ''');
await db.execute(''' await db.execute('''
CREATE INDEX idx_sessions_created_at ON ${AppConstants.sessionsTable}(created_at DESC) CREATE INDEX idx_sessions_created_at ON ${AppConstants.sessionsTable}(created_at DESC)
'''); ''');
await db.execute('''
CREATE TABLE ${AppConstants.weaponsTable} (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
caliber TEXT NOT NULL,
magazine_count INTEGER NOT NULL,
magazine_capacity INTEGER NOT NULL,
notes TEXT,
created_at TEXT NOT NULL,
optic TEXT,
silencer TEXT,
trigger TEXT,
custom_name TEXT
)
''');
await db.execute('''
CREATE TABLE ${AppConstants.maintenanceTable} (
id TEXT PRIMARY KEY,
weapon_id TEXT NOT NULL,
type TEXT NOT NULL,
description TEXT NOT NULL,
date TEXT NOT NULL,
rounds_since_last INTEGER,
FOREIGN KEY (weapon_id) REFERENCES ${AppConstants.weaponsTable}(id) ON DELETE CASCADE
)
''');
} }
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async { Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
// Handle future database migrations here if (oldVersion < 2) {
// Migration from single-target sessions to multi-target sessions
// 1. Rename existing sessions table to a temporary name
await db.execute('ALTER TABLE ${AppConstants.sessionsTable} RENAME TO sessions_old');
// 2. Create the new sessions table
await db.execute('''
CREATE TABLE ${AppConstants.sessionsTable} (
id TEXT PRIMARY KEY,
weapon TEXT NOT NULL,
max_shots_per_target INTEGER NOT NULL,
created_at TEXT NOT NULL,
notes TEXT
)
''');
// 3. Create the new target_analyses table
await db.execute('''
CREATE TABLE ${AppConstants.targetAnalysesTable} (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
target_type TEXT NOT NULL,
image_path TEXT NOT NULL,
total_score INTEGER NOT NULL,
grouping_diameter REAL,
grouping_center_x REAL,
grouping_center_y REAL,
created_at TEXT NOT NULL,
notes TEXT,
target_center_x REAL,
target_center_y REAL,
target_radius REAL,
FOREIGN KEY (session_id) REFERENCES ${AppConstants.sessionsTable}(id) ON DELETE CASCADE
)
''');
// 4. Update shots table (rename session_id to analysis_id)
await db.execute('ALTER TABLE ${AppConstants.shotsTable} RENAME TO shots_old');
await db.execute('''
CREATE TABLE ${AppConstants.shotsTable} (
id TEXT PRIMARY KEY,
x REAL NOT NULL,
y REAL NOT NULL,
score INTEGER NOT NULL,
analysis_id TEXT NOT NULL,
FOREIGN KEY (analysis_id) REFERENCES ${AppConstants.targetAnalysesTable}(id) ON DELETE CASCADE
)
''');
// 5. Migrate data
// For each old session, create one session and one target_analysis
final oldSessions = await db.query('sessions_old');
for (final old in oldSessions) {
final sessionId = old['id'] as String;
final analysisId = 'analysis_$sessionId'; // Create a unique ID for the analysis
// Insert into new sessions
await db.insert(AppConstants.sessionsTable, {
'id': sessionId,
'weapon': 'Inconnue',
'max_shots_per_target': 5,
'created_at': old['created_at'],
'notes': old['notes'],
});
// Insert into target_analyses
await db.insert(AppConstants.targetAnalysesTable, {
'id': analysisId,
'session_id': sessionId,
'target_type': old['target_type'],
'image_path': old['image_path'],
'total_score': old['total_score'],
'grouping_diameter': old['grouping_diameter'],
'grouping_center_x': old['grouping_center_x'],
'grouping_center_y': old['grouping_center_y'],
'created_at': old['created_at'],
'notes': null,
'target_center_x': old['target_center_x'],
'target_center_y': old['target_center_y'],
'target_radius': old['target_radius'],
});
// Migrate shots from shots_old to shots
final oldShots = await db.query('shots_old', where: 'session_id = ?', whereArgs: [sessionId]);
for (final shot in oldShots) {
await db.insert(AppConstants.shotsTable, {
'id': shot['id'],
'x': shot['x'],
'y': shot['y'],
'score': shot['score'],
'analysis_id': analysisId,
});
}
}
// 6. Cleanup
await db.execute('DROP TABLE sessions_old');
await db.execute('DROP TABLE shots_old');
// 7. Re-create indexes
await db.execute('''
CREATE INDEX idx_target_analyses_session_id ON ${AppConstants.targetAnalysesTable}(session_id)
''');
await db.execute('''
CREATE INDEX idx_shots_analysis_id ON ${AppConstants.shotsTable}(analysis_id)
''');
await db.execute('''
CREATE INDEX idx_sessions_created_at ON ${AppConstants.sessionsTable}(created_at DESC)
''');
}
if (oldVersion < 3) {
// Add weapons table
await db.execute('''
CREATE TABLE ${AppConstants.weaponsTable} (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
caliber TEXT NOT NULL,
magazine_count INTEGER NOT NULL,
magazine_capacity INTEGER NOT NULL,
notes TEXT,
created_at TEXT NOT NULL
)
''');
// Add weapon_id to sessions table
await db.execute('ALTER TABLE ${AppConstants.sessionsTable} ADD COLUMN weapon_id TEXT');
}
if (oldVersion < 4) {
// Add maintenance table
await db.execute('''
CREATE TABLE ${AppConstants.maintenanceTable} (
id TEXT PRIMARY KEY,
weapon_id TEXT NOT NULL,
type TEXT NOT NULL,
description TEXT NOT NULL,
date TEXT NOT NULL,
rounds_since_last INTEGER,
FOREIGN KEY (weapon_id) REFERENCES ${AppConstants.weaponsTable}(id) ON DELETE CASCADE
)
''');
}
if (oldVersion < 5) {
// Add new weapon options columns
await db.execute('ALTER TABLE ${AppConstants.weaponsTable} ADD COLUMN optic TEXT');
await db.execute('ALTER TABLE ${AppConstants.weaponsTable} ADD COLUMN silencer TEXT');
await db.execute('ALTER TABLE ${AppConstants.weaponsTable} ADD COLUMN "trigger" TEXT');
await db.execute('ALTER TABLE ${AppConstants.weaponsTable} ADD COLUMN custom_name TEXT');
}
if (oldVersion < 6) {
// Add distance column to sessions
await db.execute('ALTER TABLE ${AppConstants.sessionsTable} ADD COLUMN distance INTEGER DEFAULT 25');
}
} }
// Session operations // Session operations
@@ -84,20 +291,39 @@ class DatabaseHelper {
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
for (final shot in session.shots) { for (final analysis in session.analyses) {
// Ensure shot has correct session_id await insertTargetAnalysisTx(txn, analysis, session.id);
final shotWithSessionId = shot.copyWith(sessionId: session.id);
await txn.insert(
AppConstants.shotsTable,
shotWithSessionId.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
} }
return 1; return 1;
}); });
} }
Future<void> insertTargetAnalysis(TargetAnalysis analysis, String sessionId) async {
final db = await database;
await db.transaction((txn) async {
await insertTargetAnalysisTx(txn, analysis, sessionId);
});
}
Future<void> insertTargetAnalysisTx(Transaction txn, TargetAnalysis analysis, String sessionId) async {
final analysisWithSessionId = analysis.copyWith(sessionId: sessionId);
await txn.insert(
AppConstants.targetAnalysesTable,
analysisWithSessionId.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
for (final shot in analysis.shots) {
final shotWithAnalysisId = shot.copyWith(analysisId: analysis.id);
await txn.insert(
AppConstants.shotsTable,
shotWithAnalysisId.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
Future<Session?> getSession(String id) async { Future<Session?> getSession(String id) async {
final db = await database; final db = await database;
final sessionMaps = await db.query( final sessionMaps = await db.query(
@@ -108,106 +334,52 @@ class DatabaseHelper {
if (sessionMaps.isEmpty) return null; if (sessionMaps.isEmpty) return null;
final shotMaps = await db.query( final analysisMaps = await db.query(
AppConstants.shotsTable, AppConstants.targetAnalysesTable,
where: 'session_id = ?', where: 'session_id = ?',
whereArgs: [id], whereArgs: [id],
); );
final shots = shotMaps.map((map) => Shot.fromMap(map)).toList(); final analyses = <TargetAnalysis>[];
return Session.fromMap(sessionMaps.first, shots); for (final analysisMap in analysisMaps) {
final analysisId = analysisMap['id'] as String;
final shotMaps = await db.query(
AppConstants.shotsTable,
where: 'analysis_id = ?',
whereArgs: [analysisId],
);
final shots = shotMaps.map((map) => Shot.fromMap(map)).toList();
analyses.add(TargetAnalysis.fromMap(analysisMap, shots));
}
return Session.fromMap(sessionMaps.first, analyses);
} }
Future<List<Session>> getAllSessions({ Future<List<Session>> getAllSessions({
String? targetType,
int? limit, int? limit,
int? offset, int? offset,
}) async { }) async {
final db = await database; final db = await database;
String? whereClause;
List<dynamic>? whereArgs;
if (targetType != null) {
whereClause = 'target_type = ?';
whereArgs = [targetType];
}
final sessionMaps = await db.query( final sessionMaps = await db.query(
AppConstants.sessionsTable, AppConstants.sessionsTable,
where: whereClause,
whereArgs: whereArgs,
orderBy: 'created_at DESC', orderBy: 'created_at DESC',
limit: limit, limit: limit,
offset: offset, offset: offset,
); );
// First, check if there are orphaned shots (with empty session_id)
// and only one session - if so, assign them to that session
if (sessionMaps.length == 1) {
final orphanedShots = await db.query(
AppConstants.shotsTable,
where: 'session_id = ?',
whereArgs: [''],
);
if (orphanedShots.isNotEmpty) {
await db.update(
AppConstants.shotsTable,
{'session_id': sessionMaps.first['id']},
where: 'session_id = ?',
whereArgs: [''],
);
}
}
final sessions = <Session>[]; final sessions = <Session>[];
for (final sessionMap in sessionMaps) { for (final sessionMap in sessionMaps) {
final sessionId = sessionMap['id'] as String; final sessionId = sessionMap['id'] as String;
final session = await getSession(sessionId);
final shotMaps = await db.query( if (session != null) {
AppConstants.shotsTable, sessions.add(session);
where: 'session_id = ?', }
whereArgs: [sessionId],
);
final shots = shotMaps.map((map) => Shot.fromMap(map)).toList();
sessions.add(Session.fromMap(sessionMap, shots));
} }
return sessions; return sessions;
} }
Future<int> updateSession(Session session) async {
final db = await database;
return await db.transaction((txn) async {
await txn.update(
AppConstants.sessionsTable,
session.toMap(),
where: 'id = ?',
whereArgs: [session.id],
);
// Delete existing shots and insert new ones
await txn.delete(
AppConstants.shotsTable,
where: 'session_id = ?',
whereArgs: [session.id],
);
for (final shot in session.shots) {
// Ensure shot has correct session_id
final shotWithSessionId = shot.copyWith(sessionId: session.id);
await txn.insert(
AppConstants.shotsTable,
shotWithSessionId.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
return 1;
});
}
Future<int> deleteSession(String id) async { Future<int> deleteSession(String id) async {
final db = await database; final db = await database;
return await db.delete( return await db.delete(
@@ -229,12 +401,12 @@ class DatabaseHelper {
) ?? 0; ) ?? 0;
final avgScore = (await db.rawQuery( final avgScore = (await db.rawQuery(
'SELECT AVG(total_score) as avg FROM ${AppConstants.sessionsTable}', 'SELECT AVG(total_score) as avg FROM ${AppConstants.targetAnalysesTable}',
)).first['avg'] as double? ?? 0.0; )).first['avg'] as double? ?? 0.0;
final bestScore = Sqflite.firstIntValue( final bestScore = Sqflite.firstIntValue(
await db.rawQuery( await db.rawQuery(
'SELECT MAX(total_score) FROM ${AppConstants.sessionsTable}', 'SELECT MAX(total_score) FROM ${AppConstants.targetAnalysesTable}',
), ),
) ?? 0; ) ?? 0;
@@ -246,6 +418,121 @@ class DatabaseHelper {
}; };
} }
// Weapon operations
Future<int> insertWeapon(Weapon weapon) async {
final db = await database;
return await db.insert(
AppConstants.weaponsTable,
weapon.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<Weapon>> getAllWeapons() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(AppConstants.weaponsTable, orderBy: 'name ASC');
return List.generate(maps.length, (i) => Weapon.fromMap(maps[i]));
}
Future<Weapon?> getWeapon(String id) async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query(
AppConstants.weaponsTable,
where: 'id = ?',
whereArgs: [id],
);
if (maps.isEmpty) return null;
return Weapon.fromMap(maps.first);
}
Future<int> updateWeapon(Weapon weapon) async {
final db = await database;
// Start a transaction to ensure both updates succeed
return await db.transaction((txn) async {
// 1. Update weapon in armory
final count = await txn.update(
AppConstants.weaponsTable,
weapon.toMap(),
where: 'id = ?',
whereArgs: [weapon.id],
);
// 2. Propagate name change to all sessions linked to this weapon
await txn.update(
AppConstants.sessionsTable,
{'weapon': weapon.displayName},
where: 'weapon_id = ?',
whereArgs: [weapon.id],
);
return count;
});
}
Future<int> deleteWeapon(String id) async {
final db = await database;
return await db.transaction((txn) async {
// 1. Unlink sessions from this weapon but KEEP the name (gravé à jamais)
await txn.update(
AppConstants.sessionsTable,
{'weapon_id': null},
where: 'weapon_id = ?',
whereArgs: [id],
);
// 2. Delete the weapon record
return await txn.delete(
AppConstants.weaponsTable,
where: 'id = ?',
whereArgs: [id],
);
});
}
// Weapon statistics & Maintenance
Future<int> getRoundsFiredForWeapon(String weaponId) async {
final db = await database;
final result = await db.rawQuery('''
SELECT COUNT(s.id) as count
FROM ${AppConstants.shotsTable} s
JOIN ${AppConstants.targetAnalysesTable} ta ON s.analysis_id = ta.id
JOIN ${AppConstants.sessionsTable} sess ON ta.session_id = sess.id
WHERE sess.weapon_id = ?
''', [weaponId]);
return Sqflite.firstIntValue(result) ?? 0;
}
Future<int> insertMaintenance(MaintenanceEntry entry) async {
final db = await database;
return await db.insert(
AppConstants.maintenanceTable,
entry.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<MaintenanceEntry>> getMaintenanceForWeapon(String weaponId) async {
final db = await database;
final maps = await db.query(
AppConstants.maintenanceTable,
where: 'weapon_id = ?',
orderBy: 'date DESC',
whereArgs: [weaponId],
);
return List.generate(maps.length, (i) => MaintenanceEntry.fromMap(maps[i]));
}
Future<int> deleteMaintenance(String id) async {
final db = await database;
return await db.delete(
AppConstants.maintenanceTable,
where: 'id = ?',
whereArgs: [id],
);
}
Future<void> close() async { Future<void> close() async {
final db = await database; final db = await database;
await db.close(); await db.close();

View File

@@ -0,0 +1,57 @@
enum MaintenanceType {
cleaning('Nettoyage'),
repair('Réparation'),
inspection('Inspection'),
upgrade('Amélioration'),
other('Autre');
final String displayName;
const MaintenanceType(this.displayName);
static MaintenanceType fromString(String value) {
return MaintenanceType.values.firstWhere(
(type) => type.name == value,
orElse: () => MaintenanceType.other,
);
}
}
class MaintenanceEntry {
final String id;
final String weaponId;
final MaintenanceType type;
final String description;
final DateTime date;
final int? roundsSinceLastMaintenance;
MaintenanceEntry({
required this.id,
required this.weaponId,
required this.type,
required this.description,
required this.date,
this.roundsSinceLastMaintenance,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'weapon_id': weaponId,
'type': type.name,
'description': description,
'date': date.toIso8601String(),
'rounds_since_last': roundsSinceLastMaintenance,
};
}
factory MaintenanceEntry.fromMap(Map<String, dynamic> map) {
return MaintenanceEntry(
id: map['id'] as String,
weaponId: map['weapon_id'] as String,
type: MaintenanceType.fromString(map['type'] as String),
description: map['description'] as String,
date: DateTime.parse(map['date'] as String),
roundsSinceLastMaintenance: map['rounds_since_last'] as int?,
);
}
}

View File

@@ -1,112 +1,83 @@
import 'shot.dart'; import 'target_analysis.dart';
import 'target_type.dart';
class Session { class Session {
final String id; final String id;
final TargetType targetType; final String weapon;
final String imagePath; final String? weaponId;
final List<Shot> shots; final int maxShotsPerTarget;
final int totalScore; final List<TargetAnalysis> analyses;
final double? groupingDiameter;
final double? groupingCenterX;
final double? groupingCenterY;
final DateTime createdAt; final DateTime createdAt;
final String? notes; final String? notes;
final int distance; // Shooting distance in meters
// Target detection data
final double? targetCenterX;
final double? targetCenterY;
final double? targetRadius;
Session({ Session({
required this.id, required this.id,
required this.targetType, required this.weapon,
required this.imagePath, this.weaponId,
required this.shots, required this.maxShotsPerTarget,
required this.totalScore, required this.analyses,
this.groupingDiameter,
this.groupingCenterX,
this.groupingCenterY,
required this.createdAt, required this.createdAt,
this.notes, this.notes,
this.targetCenterX, this.distance = 25, // Default distance
this.targetCenterY,
this.targetRadius,
}); });
int get shotCount => shots.length; int get targetCount => analyses.length;
double get averageScore => shots.isEmpty ? 0.0 : totalScore / shots.length; int get totalShots => analyses.fold(0, (sum, a) => sum + a.shotCount);
int get totalScore => analyses.fold(0, (sum, a) => sum + a.totalScore);
double get averageScore => analyses.isEmpty ? 0.0 : totalScore / analyses.length;
Session copyWith({ Session copyWith({
String? id, String? id,
TargetType? targetType, String? weapon,
String? imagePath, String? weaponId,
List<Shot>? shots, int? maxShotsPerTarget,
int? totalScore, List<TargetAnalysis>? analyses,
double? groupingDiameter,
double? groupingCenterX,
double? groupingCenterY,
DateTime? createdAt, DateTime? createdAt,
String? notes, String? notes,
double? targetCenterX, int? distance,
double? targetCenterY,
double? targetRadius,
}) { }) {
return Session( return Session(
id: id ?? this.id, id: id ?? this.id,
targetType: targetType ?? this.targetType, weapon: weapon ?? this.weapon,
imagePath: imagePath ?? this.imagePath, weaponId: weaponId ?? this.weaponId,
shots: shots ?? this.shots, maxShotsPerTarget: maxShotsPerTarget ?? this.maxShotsPerTarget,
totalScore: totalScore ?? this.totalScore, analyses: analyses ?? this.analyses,
groupingDiameter: groupingDiameter ?? this.groupingDiameter,
groupingCenterX: groupingCenterX ?? this.groupingCenterX,
groupingCenterY: groupingCenterY ?? this.groupingCenterY,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
notes: notes ?? this.notes, notes: notes ?? this.notes,
targetCenterX: targetCenterX ?? this.targetCenterX, distance: distance ?? this.distance,
targetCenterY: targetCenterY ?? this.targetCenterY,
targetRadius: targetRadius ?? this.targetRadius,
); );
} }
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'id': id, 'id': id,
'target_type': targetType.name, 'weapon': weapon,
'image_path': imagePath, 'weapon_id': weaponId,
'total_score': totalScore, 'max_shots_per_target': maxShotsPerTarget,
'grouping_diameter': groupingDiameter,
'grouping_center_x': groupingCenterX,
'grouping_center_y': groupingCenterY,
'created_at': createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
'notes': notes, 'notes': notes,
'target_center_x': targetCenterX, 'distance': distance,
'target_center_y': targetCenterY,
'target_radius': targetRadius,
}; };
} }
factory Session.fromMap(Map<String, dynamic> map, List<Shot> shots) { factory Session.fromMap(Map<String, dynamic> map, List<TargetAnalysis> analyses) {
return Session( return Session(
id: map['id'] as String, id: map['id'] as String,
targetType: TargetType.fromString(map['target_type'] as String), weapon: map['weapon'] as String? ?? 'Inconnue',
imagePath: map['image_path'] as String, weaponId: map['weapon_id'] as String?,
shots: shots, maxShotsPerTarget: map['max_shots_per_target'] as int? ?? 5,
totalScore: map['total_score'] as int, analyses: analyses,
groupingDiameter: map['grouping_diameter'] as double?,
groupingCenterX: map['grouping_center_x'] as double?,
groupingCenterY: map['grouping_center_y'] as double?,
createdAt: DateTime.parse(map['created_at'] as String), createdAt: DateTime.parse(map['created_at'] as String),
notes: map['notes'] as String?, notes: map['notes'] as String?,
targetCenterX: map['target_center_x'] as double?, distance: map['distance'] as int? ?? 25,
targetCenterY: map['target_center_y'] as double?,
targetRadius: map['target_radius'] as double?,
); );
} }
@override @override
String toString() { String toString() {
return 'Session(id: $id, targetType: $targetType, shots: ${shots.length}, totalScore: $totalScore, createdAt: $createdAt)'; return 'Session(id: $id, weapon: $weapon, distance: ${distance}m, targets: ${analyses.length}, createdAt: $createdAt)';
} }
} }

View File

@@ -3,14 +3,14 @@ class Shot {
final double x; // Relative position (0.0 - 1.0) final double x; // Relative position (0.0 - 1.0)
final double y; // Relative position (0.0 - 1.0) final double y; // Relative position (0.0 - 1.0)
final int score; final int score;
final String sessionId; final String analysisId;
Shot({ Shot({
required this.id, required this.id,
required this.x, required this.x,
required this.y, required this.y,
required this.score, required this.score,
required this.sessionId, required this.analysisId,
}); });
Shot copyWith({ Shot copyWith({
@@ -18,14 +18,14 @@ class Shot {
double? x, double? x,
double? y, double? y,
int? score, int? score,
String? sessionId, String? analysisId,
}) { }) {
return Shot( return Shot(
id: id ?? this.id, id: id ?? this.id,
x: x ?? this.x, x: x ?? this.x,
y: y ?? this.y, y: y ?? this.y,
score: score ?? this.score, score: score ?? this.score,
sessionId: sessionId ?? this.sessionId, analysisId: analysisId ?? this.analysisId,
); );
} }
@@ -35,7 +35,7 @@ class Shot {
'x': x, 'x': x,
'y': y, 'y': y,
'score': score, 'score': score,
'session_id': sessionId, 'analysis_id': analysisId,
}; };
} }
@@ -45,13 +45,13 @@ class Shot {
x: (map['x'] as num).toDouble(), x: (map['x'] as num).toDouble(),
y: (map['y'] as num).toDouble(), y: (map['y'] as num).toDouble(),
score: map['score'] as int, score: map['score'] as int,
sessionId: map['session_id'] as String, analysisId: map['analysis_id'] as String? ?? map['session_id'] as String? ?? '',
); );
} }
@override @override
String toString() { String toString() {
return 'Shot(id: $id, x: $x, y: $y, score: $score, sessionId: $sessionId)'; return 'Shot(id: $id, x: $x, y: $y, score: $score, analysisId: $analysisId)';
} }
@override @override
@@ -62,11 +62,11 @@ class Shot {
other.x == x && other.x == x &&
other.y == y && other.y == y &&
other.score == score && other.score == score &&
other.sessionId == sessionId; other.analysisId == analysisId;
} }
@override @override
int get hashCode { int get hashCode {
return id.hashCode ^ x.hashCode ^ y.hashCode ^ score.hashCode ^ sessionId.hashCode; return id.hashCode ^ x.hashCode ^ y.hashCode ^ score.hashCode ^ analysisId.hashCode;
} }
} }

View File

@@ -0,0 +1,113 @@
import 'shot.dart';
import 'target_type.dart';
class TargetAnalysis {
final String id;
final String sessionId;
final TargetType targetType;
final String imagePath;
final List<Shot> shots;
final int totalScore;
final double? groupingDiameter;
final double? groupingCenterX;
final double? groupingCenterY;
final DateTime createdAt;
final String? notes;
// Target detection data
final double? targetCenterX;
final double? targetCenterY;
final double? targetRadius;
TargetAnalysis({
required this.id,
required this.sessionId,
required this.targetType,
required this.imagePath,
required this.shots,
required this.totalScore,
this.groupingDiameter,
this.groupingCenterX,
this.groupingCenterY,
required this.createdAt,
this.notes,
this.targetCenterX,
this.targetCenterY,
this.targetRadius,
});
int get shotCount => shots.length;
double get averageScore => shots.isEmpty ? 0.0 : totalScore / shots.length;
TargetAnalysis copyWith({
String? id,
String? sessionId,
TargetType? targetType,
String? imagePath,
List<Shot>? shots,
int? totalScore,
double? groupingDiameter,
double? groupingCenterX,
double? groupingCenterY,
DateTime? createdAt,
String? notes,
double? targetCenterX,
double? targetCenterY,
double? targetRadius,
}) {
return TargetAnalysis(
id: id ?? this.id,
sessionId: sessionId ?? this.sessionId,
targetType: targetType ?? this.targetType,
imagePath: imagePath ?? this.imagePath,
shots: shots ?? this.shots,
totalScore: totalScore ?? this.totalScore,
groupingDiameter: groupingDiameter ?? this.groupingDiameter,
groupingCenterX: groupingCenterX ?? this.groupingCenterX,
groupingCenterY: groupingCenterY ?? this.groupingCenterY,
createdAt: createdAt ?? this.createdAt,
notes: notes ?? this.notes,
targetCenterX: targetCenterX ?? this.targetCenterX,
targetCenterY: targetCenterY ?? this.targetCenterY,
targetRadius: targetRadius ?? this.targetRadius,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'session_id': sessionId,
'target_type': targetType.name,
'image_path': imagePath,
'total_score': totalScore,
'grouping_diameter': groupingDiameter,
'grouping_center_x': groupingCenterX,
'grouping_center_y': groupingCenterY,
'created_at': createdAt.toIso8601String(),
'notes': notes,
'target_center_x': targetCenterX,
'target_center_y': targetCenterY,
'target_radius': targetRadius,
};
}
factory TargetAnalysis.fromMap(Map<String, dynamic> map, List<Shot> shots) {
return TargetAnalysis(
id: map['id'] as String,
sessionId: map['session_id'] as String,
targetType: TargetType.fromString(map['target_type'] as String),
imagePath: map['image_path'] as String,
shots: shots,
totalScore: map['total_score'] as int,
groupingDiameter: map['grouping_diameter'] as double?,
groupingCenterX: map['grouping_center_x'] as double?,
groupingCenterY: map['grouping_center_y'] as double?,
createdAt: DateTime.parse(map['created_at'] as String),
notes: map['notes'] as String?,
targetCenterX: map['target_center_x'] as double?,
targetCenterY: map['target_center_y'] as double?,
targetRadius: map['target_radius'] as double?,
);
}
}

114
lib/data/models/weapon.dart Normal file
View File

@@ -0,0 +1,114 @@
enum WeaponType {
handgun('Arme de Poing'),
rifle('Arme d\'Épaule'),
shotgun('Fusil à Pompe'),
airgun('Airsoft / Airgun');
final String displayName;
const WeaponType(this.displayName);
static WeaponType fromString(String value) {
return WeaponType.values.firstWhere(
(type) => type.name == value,
orElse: () => WeaponType.handgun,
);
}
}
class Weapon {
final String id;
final String name;
final WeaponType type;
final String caliber;
final int magazineCount;
final int magazineCapacity;
final String? notes;
final DateTime createdAt;
// New options
final String? optic;
final String? silencer;
final String? trigger;
final String? customName;
String get displayName => (customName != null && customName!.isNotEmpty) ? customName! : name;
Weapon({
required this.id,
required this.name,
required this.type,
required this.caliber,
required this.magazineCount,
required this.magazineCapacity,
this.notes,
required this.createdAt,
this.optic,
this.silencer,
this.trigger,
this.customName,
});
Weapon copyWith({
String? id,
String? name,
WeaponType? type,
String? caliber,
int? magazineCount,
int? magazineCapacity,
String? notes,
DateTime? createdAt,
String? optic,
String? silencer,
String? trigger,
String? customName,
}) {
return Weapon(
id: id ?? this.id,
name: name ?? this.name,
type: type ?? this.type,
caliber: caliber ?? this.caliber,
magazineCount: magazineCount ?? this.magazineCount,
magazineCapacity: magazineCapacity ?? this.magazineCapacity,
notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt,
optic: optic ?? this.optic,
silencer: silencer ?? this.silencer,
trigger: trigger ?? this.trigger,
customName: customName ?? this.customName,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'type': type.name,
'caliber': caliber,
'magazine_count': magazineCount,
'magazine_capacity': magazineCapacity,
'notes': notes,
'created_at': createdAt.toIso8601String(),
'optic': optic,
'silencer': silencer,
'trigger': trigger,
'custom_name': customName,
};
}
factory Weapon.fromMap(Map<String, dynamic> map) {
return Weapon(
id: map['id'] as String,
name: map['name'] as String,
type: WeaponType.fromString(map['type'] as String),
caliber: map['caliber'] as String,
magazineCount: map['magazine_count'] as int,
magazineCapacity: map['magazine_capacity'] as int,
notes: map['notes'] as String?,
createdAt: DateTime.parse(map['created_at'] as String),
optic: map['optic'] as String?,
silencer: map['silencer'] as String?,
trigger: map['trigger'] as String?,
customName: map['custom_name'] as String?,
);
}
}

View File

@@ -4,8 +4,10 @@ import 'package:path/path.dart' as path;
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../database/database_helper.dart'; import '../database/database_helper.dart';
import '../models/session.dart'; import '../models/session.dart';
import '../models/target_analysis.dart';
import '../models/shot.dart'; import '../models/shot.dart';
import '../models/target_type.dart'; import '../models/weapon.dart';
import '../models/maintenance.dart';
class SessionRepository { class SessionRepository {
final DatabaseHelper _databaseHelper; final DatabaseHelper _databaseHelper;
@@ -18,8 +20,37 @@ class SessionRepository {
_uuid = uuid ?? const Uuid(); _uuid = uuid ?? const Uuid();
Future<Session> createSession({ Future<Session> createSession({
required TargetType targetType, String? id,
required String weapon,
String? weaponId,
required int maxShotsPerTarget,
required List<TargetAnalysis> analyses,
String? notes,
int distance = 25,
}) async {
final session = Session(
id: id ?? _uuid.v4(),
weapon: weapon,
weaponId: weaponId,
maxShotsPerTarget: maxShotsPerTarget,
analyses: analyses,
createdAt: DateTime.now(),
notes: notes,
distance: distance,
);
await _databaseHelper.insertSession(session);
return session;
}
Future<void> addAnalysisToSession(String sessionId, TargetAnalysis analysis) async {
await _databaseHelper.insertTargetAnalysis(analysis, sessionId);
}
Future<TargetAnalysis> prepareTargetAnalysis({
required String imagePath, required String imagePath,
required String sessionId,
required dynamic targetType, // TargetType
required List<Shot> shots, required List<Shot> shots,
required int totalScore, required int totalScore,
double? groupingDiameter, double? groupingDiameter,
@@ -33,8 +64,9 @@ class SessionRepository {
// Copy image to app documents directory // Copy image to app documents directory
final savedImagePath = await _saveImage(imagePath); final savedImagePath = await _saveImage(imagePath);
final session = Session( return TargetAnalysis(
id: _uuid.v4(), id: _uuid.v4(),
sessionId: sessionId,
targetType: targetType, targetType: targetType,
imagePath: savedImagePath, imagePath: savedImagePath,
shots: shots, shots: shots,
@@ -48,9 +80,6 @@ class SessionRepository {
targetCenterY: targetCenterY, targetCenterY: targetCenterY,
targetRadius: targetRadius, targetRadius: targetRadius,
); );
await _databaseHelper.insertSession(session);
return session;
} }
Future<String> _saveImage(String sourcePath) async { Future<String> _saveImage(String sourcePath) async {
@@ -75,28 +104,23 @@ class SessionRepository {
} }
Future<List<Session>> getAllSessions({ Future<List<Session>> getAllSessions({
TargetType? targetType,
int? limit, int? limit,
int? offset, int? offset,
}) async { }) async {
return await _databaseHelper.getAllSessions( return await _databaseHelper.getAllSessions(
targetType: targetType?.name,
limit: limit, limit: limit,
offset: offset, offset: offset,
); );
} }
Future<void> updateSession(Session session) async {
await _databaseHelper.updateSession(session);
}
Future<void> deleteSession(String id) async { Future<void> deleteSession(String id) async {
final session = await getSession(id); final session = await getSession(id);
if (session != null) { if (session != null) {
// Delete the image file for (final analysis in session.analyses) {
final imageFile = File(session.imagePath); final imageFile = File(analysis.imagePath);
if (await imageFile.exists()) { if (await imageFile.exists()) {
await imageFile.delete(); await imageFile.delete();
}
} }
} }
await _databaseHelper.deleteSession(id); await _databaseHelper.deleteSession(id);
@@ -106,7 +130,79 @@ class SessionRepository {
return await _databaseHelper.getStatistics(); return await _databaseHelper.getStatistics();
} }
String generateShotId() { String generateId() {
return _uuid.v4(); return _uuid.v4();
} }
// Weapon Repository methods
Future<Weapon> addWeapon({
required String name,
required WeaponType type,
required String caliber,
required int magazineCount,
required int magazineCapacity,
String? notes,
String? optic,
String? silencer,
String? trigger,
String? customName,
}) async {
final weapon = Weapon(
id: _uuid.v4(),
name: name,
type: type,
caliber: caliber,
magazineCount: magazineCount,
magazineCapacity: magazineCapacity,
notes: notes,
createdAt: DateTime.now(),
optic: optic,
silencer: silencer,
trigger: trigger,
customName: customName,
);
await _databaseHelper.insertWeapon(weapon);
return weapon;
}
Future<List<Weapon>> getWeapons() async {
return await _databaseHelper.getAllWeapons();
}
Future<void> updateWeapon(Weapon weapon) async {
await _databaseHelper.updateWeapon(weapon);
}
Future<void> deleteWeapon(String id) async {
await _databaseHelper.deleteWeapon(id);
}
Future<int> getRoundsFiredForWeapon(String weaponId) async {
return await _databaseHelper.getRoundsFiredForWeapon(weaponId);
}
Future<void> addMaintenanceEntry({
required String weaponId,
required MaintenanceType type,
required String description,
int? roundsSinceLast,
}) async {
final entry = MaintenanceEntry(
id: _uuid.v4(),
weaponId: weaponId,
type: type,
description: description,
date: DateTime.now(),
roundsSinceLastMaintenance: roundsSinceLast,
);
await _databaseHelper.insertMaintenance(entry);
}
Future<List<MaintenanceEntry>> getMaintenanceHistory(String weaponId) async {
return await _databaseHelper.getMaintenanceForWeapon(weaponId);
}
Future<void> deleteMaintenanceEntry(String id) async {
await _databaseHelper.deleteMaintenance(id);
}
} }

View File

@@ -7,9 +7,9 @@ library;
import 'dart:io'; import 'dart:io';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../../data/models/session.dart'; import '../../data/models/target_analysis.dart';
import '../../data/models/shot.dart'; import '../../data/models/shot.dart';
import '../../data/models/target_type.dart'; import '../../data/models/target_type.dart';
import '../../data/repositories/session_repository.dart'; import '../../data/repositories/session_repository.dart';
@@ -120,6 +120,7 @@ class AnalysisProvider extends ChangeNotifier {
String imagePath, String imagePath,
TargetType targetType, { TargetType targetType, {
bool autoAnalyze = true, bool autoAnalyze = true,
Offset? manualCenter,
}) async { }) async {
_state = AnalysisState.loading; _state = AnalysisState.loading;
_imagePath = imagePath; _imagePath = imagePath;
@@ -138,8 +139,8 @@ class AnalysisProvider extends ChangeNotifier {
if (!autoAnalyze) { if (!autoAnalyze) {
// Just setup default values without running detection // Just setup default values without running detection
_targetCenterX = 0.5; _targetCenterX = manualCenter?.dx ?? 0.5;
_targetCenterY = 0.5; _targetCenterY = manualCenter?.dy ?? 0.5;
_targetRadius = 0.4; _targetRadius = 0.4;
_targetInnerRadius = 0.04; _targetInnerRadius = 0.04;
@@ -173,7 +174,7 @@ class AnalysisProvider extends ChangeNotifier {
x: impact.x, x: impact.x,
y: impact.y, y: impact.y,
score: impact.suggestedScore, score: impact.suggestedScore,
sessionId: '', // Will be set when saving analysisId: '', // Will be set when saving
); );
}).toList(); }).toList();
@@ -195,7 +196,7 @@ class AnalysisProvider extends ChangeNotifier {
/// Add a manual shot /// Add a manual shot
void addShot(double x, double y) { void addShot(double x, double y) {
final score = _calculateShotScore(x, y); final score = _calculateShotScore(x, y);
final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, sessionId: ''); final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, analysisId: '');
_shots.add(shot); _shots.add(shot);
_recalculateScores(); _recalculateScores();
@@ -275,7 +276,7 @@ class AnalysisProvider extends ChangeNotifier {
x: impact.x, x: impact.x,
y: impact.y, y: impact.y,
score: score, score: score,
sessionId: '', analysisId: '',
); );
_shots.add(shot); _shots.add(shot);
} }
@@ -288,14 +289,6 @@ class AnalysisProvider extends ChangeNotifier {
} }
/// Auto-detect impacts using OpenCV (Hough Circles + Contours) /// Auto-detect impacts using OpenCV (Hough Circles + Contours)
///
/// NOTE: OpenCV est actuellement désactivé sur Windows en raison de problèmes
/// de compilation. Cette méthode retourne 0 (aucun impact détecté).
/// Utiliser autoDetectImpacts() à la place.
///
/// Utilise les algorithmes OpenCV pour une détection plus robuste:
/// - Transformation de Hough pour détecter les cercles
/// - Analyse de contours avec filtrage par circularité
Future<int> autoDetectImpactsWithOpenCV({ Future<int> autoDetectImpactsWithOpenCV({
double cannyThreshold1 = 50, double cannyThreshold1 = 50,
double cannyThreshold2 = 150, double cannyThreshold2 = 150,
@@ -352,7 +345,7 @@ class AnalysisProvider extends ChangeNotifier {
x: impact.x, x: impact.x,
y: impact.y, y: impact.y,
score: score, score: score,
sessionId: '', analysisId: '',
); );
_shots.add(shot); _shots.add(shot);
} }
@@ -404,7 +397,7 @@ class AnalysisProvider extends ChangeNotifier {
x: impact.x, x: impact.x,
y: impact.y, y: impact.y,
score: score, score: score,
sessionId: '', analysisId: '',
); );
_shots.add(shot); _shots.add(shot);
} }
@@ -419,7 +412,7 @@ class AnalysisProvider extends ChangeNotifier {
/// Add a reference impact for calibrated detection /// Add a reference impact for calibrated detection
void addReferenceImpact(double x, double y) { void addReferenceImpact(double x, double y) {
final score = _calculateShotScore(x, y); final score = _calculateShotScore(x, y);
final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, sessionId: ''); final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, analysisId: '');
_referenceImpacts.add(shot); _referenceImpacts.add(shot);
notifyListeners(); notifyListeners();
} }
@@ -489,7 +482,7 @@ class AnalysisProvider extends ChangeNotifier {
x: impact.x, x: impact.x,
y: impact.y, y: impact.y,
score: score, score: score,
sessionId: '', analysisId: '',
); );
_shots.add(shot); _shots.add(shot);
} }
@@ -563,7 +556,7 @@ class AnalysisProvider extends ChangeNotifier {
} }
return false; return false;
} catch (e) { } catch (e) {
print('Auto-calibration error: $e'); debugPrint('Auto-calibration error: $e');
return false; return false;
} }
} }
@@ -608,7 +601,6 @@ class AnalysisProvider extends ChangeNotifier {
} }
} }
/* version deux a tester*/
/// Calcule ET applique la correction pour un feedback immédiat /// Calcule ET applique la correction pour un feedback immédiat
Future<void> calculateAndApplyDistortion() async { Future<void> calculateAndApplyDistortion() async {
// 1. Calcul des paramètres (votre code actuel) // 1. Calcul des paramètres (votre code actuel)
@@ -644,7 +636,6 @@ class AnalysisProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
} }
/* fin section deux a tester*/
int _calculateShotScore(double x, double y) { int _calculateShotScore(double x, double y) {
if (_targetType == TargetType.concentric) { if (_targetType == TargetType.concentric) {
@@ -697,10 +688,10 @@ class AnalysisProvider extends ChangeNotifier {
return false; return false;
} }
// Identifiant de session temporaire si non sauvegardée // Identifiant d'analyse temporaire
final sessionId = _shots.isNotEmpty && _shots.first.sessionId.isNotEmpty final analysisId = _shots.isNotEmpty && _shots.first.analysisId.isNotEmpty
? _shots.first.sessionId ? _shots.first.analysisId
: 'session_${DateTime.now().millisecondsSinceEpoch}'; : 'analysis_${DateTime.now().millisecondsSinceEpoch}';
final service = AiExportService(); // Local instanciation for simplicity final service = AiExportService(); // Local instanciation for simplicity
@@ -709,7 +700,7 @@ class AnalysisProvider extends ChangeNotifier {
final success = await service.exportData( final success = await service.exportData(
imagePath: _imagePath!, imagePath: _imagePath!,
sessionId: sessionId, sessionId: 'export',
targetType: _targetType!, targetType: _targetType!,
targetCenterX: _targetCenterX, targetCenterX: _targetCenterX,
targetCenterY: _targetCenterY, targetCenterY: _targetCenterY,
@@ -725,16 +716,23 @@ class AnalysisProvider extends ChangeNotifier {
return success; return success;
} }
/// Save the session /// Save the session (legacy standalone flow)
Future<Session> saveSession({String? notes}) async { Future<TargetAnalysis> saveSession({
String? notes,
String? sessionId,
String? weaponName,
String? weaponId,
int? distance,
}) async {
if (_imagePath == null || _targetType == null) { if (_imagePath == null || _targetType == null) {
throw Exception('Cannot save: missing image or target type'); throw Exception('Cannot save: missing image or target type');
} }
final session = await _sessionRepository.createSession( final targetAnalysis = await _sessionRepository.prepareTargetAnalysis(
targetType: _targetType!,
imagePath: _imagePath!, imagePath: _imagePath!,
shots: _shots.map((s) => s.copyWith(sessionId: '')).toList(), sessionId: sessionId ?? 'standalone',
targetType: _targetType!,
shots: _shots,
totalScore: totalScore, totalScore: totalScore,
groupingDiameter: _groupingResult?.diameter, groupingDiameter: _groupingResult?.diameter,
groupingCenterX: _groupingResult?.centerX, groupingCenterX: _groupingResult?.centerX,
@@ -745,11 +743,34 @@ class AnalysisProvider extends ChangeNotifier {
targetRadius: _targetRadius, targetRadius: _targetRadius,
); );
// Update shots with session ID if (sessionId != null && sessionId != 'standalone') {
_shots = session.shots; final existingSession = await _sessionRepository.getSession(sessionId);
notifyListeners(); if (existingSession != null) {
await _sessionRepository.addAnalysisToSession(sessionId, targetAnalysis);
} else {
await _sessionRepository.createSession(
id: sessionId,
weapon: weaponName ?? 'Inconnue',
weaponId: weaponId,
maxShotsPerTarget: shotCount,
analyses: [targetAnalysis],
notes: notes,
distance: distance ?? 25,
);
}
} else {
await _sessionRepository.createSession(
weapon: weaponName ?? 'Inconnue',
weaponId: weaponId,
maxShotsPerTarget: shotCount,
analyses: [targetAnalysis],
notes: notes,
distance: distance ?? 25,
);
}
return session; notifyListeners();
return targetAnalysis;
} }
/// Reset the provider /// Reset the provider

File diff suppressed because it is too large Load Diff

View File

@@ -40,10 +40,10 @@ class TargetCalibration extends StatefulWidget {
}); });
@override @override
State<TargetCalibration> createState() => _TargetCalibrationState(); State<TargetCalibration> createState() => TargetCalibrationState();
} }
class _TargetCalibrationState extends State<TargetCalibration> { class TargetCalibrationState extends State<TargetCalibration> {
late double _centerX; late double _centerX;
late double _centerY; late double _centerY;
late double _radius; late double _radius;
@@ -66,8 +66,8 @@ class _TargetCalibrationState extends State<TargetCalibration> {
_initRingRadii(); _initRingRadii();
} }
void _initRingRadii() { void _initRingRadii({bool forceRecalculate = false}) {
if (widget.initialRingRadii != null && if (!forceRecalculate && widget.initialRingRadii != null &&
widget.initialRingRadii!.length == _ringCount) { widget.initialRingRadii!.length == _ringCount) {
_ringRadii = List.from(widget.initialRingRadii!); _ringRadii = List.from(widget.initialRingRadii!);
} else { } else {
@@ -121,75 +121,188 @@ class _TargetCalibrationState extends State<TargetCalibration> {
builder: (context, constraints) { builder: (context, constraints) {
final size = constraints.biggest; final size = constraints.biggest;
return GestureDetector( return Stack(
onPanStart: (details) => _onPanStart(details, size), children: [
onPanUpdate: (details) => _onPanUpdate(details, size), GestureDetector(
onPanEnd: (_) => _onPanEnd(), onPanStart: (details) => _onPanStart(details, size),
child: CustomPaint( onPanUpdate: (details) => _onPanUpdate(details, size),
size: size, onPanEnd: (_) => _onPanEnd(),
painter: _CalibrationPainter( child: CustomPaint(
centerX: _centerX, size: size,
centerY: _centerY, painter: _CalibrationPainter(
radius: _radius, centerX: _centerX,
innerRadius: _innerRadius, centerY: _centerY,
ringCount: _ringCount, radius: _radius,
ringRadii: _ringRadii, innerRadius: _innerRadius,
targetType: widget.targetType, ringCount: _ringCount,
isDraggingCenter: _isDraggingCenter, ringRadii: _ringRadii,
isDraggingRadius: _isDraggingRadius, targetType: widget.targetType,
isDraggingInnerRadius: _isDraggingInnerRadius, isDraggingCenter: _isDraggingCenter,
isDraggingRadius: false, // Plus de drag direct du rayon
isDraggingInnerRadius: false,
),
),
), ),
),
// Slider pour le rayon (agrandissement)
Positioned(
top: 10,
left: 40,
right: 40,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
const Text('Taille ', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold)),
const Icon(Icons.zoom_out, color: Colors.white, size: 16),
Expanded(
child: Slider(
value: _radius.clamp(0.05, 3.0),
min: 0.05,
max: 3.0,
activeColor: AppTheme.primaryColor,
onChanged: (value) {
setState(() {
_radius = value;
// On force le recalcul car on change la taille
_initRingRadii(forceRecalculate: true);
});
_notifyChange();
},
),
),
const Icon(Icons.zoom_in, color: Colors.white, size: 16),
],
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
const Text('Espacement ', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold)),
const Icon(Icons.compress, color: Colors.white, size: 16),
Expanded(
child: Slider(
value: (_innerRadius / _radius).clamp(0.01, 0.9),
min: 0.01,
max: 0.9,
activeColor: Colors.orange,
onChanged: (value) {
setState(() {
// On ajuste l'innerRadius comme un ratio du radius global
_innerRadius = _radius * value;
_initRingRadii(forceRecalculate: true);
});
_notifyChange();
},
),
),
const Icon(Icons.expand, color: Colors.white, size: 16),
],
),
),
],
),
),
],
); );
}, },
); );
} }
Widget buildDirectionalControls(BuildContext context, Size size) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white10),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildDirectionButton(
Icons.keyboard_arrow_up,
() => _moveCenterByPixels(0, -1, size),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildDirectionButton(
Icons.keyboard_arrow_left,
() => _moveCenterByPixels(-1, 0, size),
),
const SizedBox(width: 40),
_buildDirectionButton(
Icons.keyboard_arrow_right,
() => _moveCenterByPixels(1, 0, size),
),
],
),
_buildDirectionButton(
Icons.keyboard_arrow_down,
() => _moveCenterByPixels(0, 1, size),
),
],
),
);
}
Widget _buildDirectionButton(IconData icon, VoidCallback onPressed) {
return Container(
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
),
margin: const EdgeInsets.all(2),
child: IconButton(
icon: Icon(icon, color: Colors.white, size: 28),
onPressed: onPressed,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
),
);
}
void _notifyChange() {
widget.onCalibrationChanged(
_centerX,
_centerY,
_innerRadius,
_radius,
_ringCount,
ringRadii: _ringRadii,
);
}
void _moveCenterByPixels(double dx, double dy, Size size) {
setState(() {
_centerX = _centerX + (dx / size.width);
_centerY = _centerY + (dy / size.height);
});
_notifyChange();
}
void _onPanStart(DragStartDetails details, Size size) { void _onPanStart(DragStartDetails details, Size size) {
final tapX = details.localPosition.dx / size.width; final tapX = details.localPosition.dx / size.width;
final tapY = details.localPosition.dy / size.height; final tapY = details.localPosition.dy / size.height;
// Check if tapping on center handle
final distToCenter = _distance(tapX, tapY, _centerX, _centerY); final distToCenter = _distance(tapX, tapY, _centerX, _centerY);
// Check if tapping on outer radius handle if (distToCenter < 0.05 || distToCenter < _radius + 0.02) {
final minDim = math.min(size.width, size.height);
final outerRadius = _radius;
final radiusHandleX = _centerX + outerRadius * minDim / size.width;
final radiusHandleY = _centerY;
final distToOuterHandle = _distance(
tapX,
tapY,
radiusHandleX.clamp(0.0, 1.0),
radiusHandleY.clamp(0.0, 1.0),
);
// Check if tapping on inner radius handle (top edge of innermost circle)
final actualInnerRadius = _innerRadius;
final innerHandleX = _centerX;
final innerHandleY = _centerY - actualInnerRadius * minDim / size.height;
final distToInnerHandle = _distance(
tapX,
tapY,
innerHandleX.clamp(0.0, 1.0),
innerHandleY.clamp(0.0, 1.0),
);
// Increase touch target size slightly for handles
if (distToCenter < 0.05) {
setState(() {
_isDraggingCenter = true;
});
} else if (distToOuterHandle < 0.05) {
setState(() {
_isDraggingRadius = true;
});
} else if (distToInnerHandle < 0.05) {
setState(() {
_isDraggingInnerRadius = true;
});
} else if (distToCenter < _radius + 0.02) {
// Tapping inside the target - move center
setState(() { setState(() {
_isDraggingCenter = true; _isDraggingCenter = true;
}); });
@@ -206,30 +319,10 @@ class _TargetCalibrationState extends State<TargetCalibration> {
// Move center // Move center
_centerX = _centerX + deltaX; _centerX = _centerX + deltaX;
_centerY = _centerY + deltaY; _centerY = _centerY + deltaY;
} else if (_isDraggingRadius) {
// Adjust outer radius
final newRadius = _radius + deltaX * (size.width / minDim);
_radius = newRadius.clamp(math.max(0.05, _innerRadius + 0.01), 3.0);
_initRingRadii(); // Recalculate linear separation
} else if (_isDraggingInnerRadius) {
// Adjust inner radius (sliding up reduces Y, so deltaY is negative when growing. Thus we subtract deltaY)
final newInnerRadius = _innerRadius - deltaY * (size.height / minDim);
_innerRadius = newInnerRadius.clamp(
0.01,
math.max(0.01, _radius - 0.01),
);
_initRingRadii(); // Recalculate linear separation
} }
}); });
widget.onCalibrationChanged( _notifyChange();
_centerX,
_centerY,
_innerRadius,
_radius,
_ringCount,
ringRadii: _ringRadii,
);
} }
void _onPanEnd() { void _onPanEnd() {
@@ -288,7 +381,7 @@ class _CalibrationPainter extends CustomPainter {
// Fullscreen crosshairs when dragging center // Fullscreen crosshairs when dragging center
if (isDraggingCenter) { if (isDraggingCenter) {
final crosshairLinePaint = Paint() final crosshairLinePaint = Paint()
..color = AppTheme.successColor.withValues(alpha: 0.5) ..color = Colors.red.withValues(alpha: 0.5)
..strokeWidth = 1; ..strokeWidth = 1;
canvas.drawLine( canvas.drawLine(
Offset(0, centerPx.dy), Offset(0, centerPx.dy),
@@ -305,12 +398,6 @@ class _CalibrationPainter extends CustomPainter {
// Draw center handle // Draw center handle
_drawCenterHandle(canvas, centerPx); _drawCenterHandle(canvas, centerPx);
// Draw radius handle (for outer ring)
_drawRadiusHandle(canvas, size, centerPx, baseRadiusPx);
// Draw inner radius handle
_drawInnerRadiusHandle(canvas, size, centerPx, innerRadiusPx);
// Draw instructions // Draw instructions
_drawInstructions(canvas, size); _drawInstructions(canvas, size);
} }
@@ -342,7 +429,7 @@ class _CalibrationPainter extends CustomPainter {
final strokePaint = Paint() final strokePaint = Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 1 ..strokeWidth = 1
..color = Colors.white.withValues(alpha: 0.6); ..color = Colors.red.withValues(alpha: 0.8);
// Draw from outside to inside // Draw from outside to inside
for (int i = ringCount - 1; i >= 0; i--) { for (int i = ringCount - 1; i >= 0; i--) {
@@ -426,20 +513,20 @@ class _CalibrationPainter extends CustomPainter {
void _drawCenterHandle(Canvas canvas, Offset center) { void _drawCenterHandle(Canvas canvas, Offset center) {
// Outer circle // Outer circle
final outerPaint = Paint() final outerPaint = Paint()
..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor ..color = isDraggingCenter ? Colors.redAccent : Colors.red
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 3; ..strokeWidth = 3;
canvas.drawCircle(center, 15, outerPaint); canvas.drawCircle(center, 15, outerPaint);
// Inner dot // Inner dot
final innerPaint = Paint() final innerPaint = Paint()
..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor ..color = isDraggingCenter ? Colors.redAccent : Colors.red
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
canvas.drawCircle(center, 5, innerPaint); canvas.drawCircle(center, 5, innerPaint);
// Crosshair // Crosshair
final crossPaint = Paint() final crossPaint = Paint()
..color = isDraggingCenter ? AppTheme.successColor : AppTheme.primaryColor ..color = isDraggingCenter ? Colors.redAccent : Colors.red
..strokeWidth = 2; ..strokeWidth = 2;
canvas.drawLine( canvas.drawLine(
Offset(center.dx - 20, center.dy), Offset(center.dx - 20, center.dy),
@@ -463,151 +550,6 @@ class _CalibrationPainter extends CustomPainter {
); );
} }
void _drawRadiusHandle(
Canvas canvas,
Size size,
Offset center,
double baseRadius,
) {
// Radius handle on the right edge of the outermost ring
final actualHandleX = center.dx + baseRadius;
final clampedHandleX = actualHandleX.clamp(20.0, size.width - 20);
final clampedHandleY = center.dy.clamp(20.0, size.height - 20);
final handlePos = Offset(clampedHandleX, clampedHandleY);
// Check if handle is clamped (radius extends beyond visible area)
final isClamped = actualHandleX > size.width - 20;
final paint = Paint()
..color = isDraggingRadius
? AppTheme.successColor
: (isClamped ? Colors.orange : AppTheme.warningColor)
..style = PaintingStyle.fill;
// Draw handle as a small circle with arrows
canvas.drawCircle(handlePos, 14, paint);
// Draw arrow indicators
final arrowPaint = Paint()
..color = Colors.white
..strokeWidth = 2
..style = PaintingStyle.stroke;
// Left arrow
canvas.drawLine(
Offset(handlePos.dx - 4, handlePos.dy),
Offset(handlePos.dx - 8, handlePos.dy - 4),
arrowPaint,
);
canvas.drawLine(
Offset(handlePos.dx - 4, handlePos.dy),
Offset(handlePos.dx - 8, handlePos.dy + 4),
arrowPaint,
);
// Right arrow
canvas.drawLine(
Offset(handlePos.dx + 4, handlePos.dy),
Offset(handlePos.dx + 8, handlePos.dy - 4),
arrowPaint,
);
canvas.drawLine(
Offset(handlePos.dx + 4, handlePos.dy),
Offset(handlePos.dx + 8, handlePos.dy + 4),
arrowPaint,
);
// Label
final textPainter = TextPainter(
text: const TextSpan(
text: 'EXT.',
style: TextStyle(
color: Colors.white,
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(handlePos.dx - textPainter.width / 2, handlePos.dy + 16),
);
}
void _drawInnerRadiusHandle(
Canvas canvas,
Size size,
Offset center,
double innerRadiusPx,
) {
// Inner radius handle on the top edge of the innermost ring
final actualHandleY = center.dy - innerRadiusPx;
final clampedHandleX = center.dx.clamp(20.0, size.width - 20);
final clampedHandleY = actualHandleY.clamp(20.0, size.height - 20);
final handlePos = Offset(clampedHandleX, clampedHandleY);
final isClamped = actualHandleY < 20.0;
final paint = Paint()
..color = isDraggingInnerRadius
? AppTheme.successColor
: (isClamped ? Colors.orange : Colors.purpleAccent)
..style = PaintingStyle.fill;
// Draw handle
canvas.drawCircle(handlePos, 14, paint);
// Up/Down arrow indicators
final arrowPaint = Paint()
..color = Colors.white
..strokeWidth = 2
..style = PaintingStyle.stroke;
// Up arrow
canvas.drawLine(
Offset(handlePos.dx, handlePos.dy - 4),
Offset(handlePos.dx - 4, handlePos.dy - 8),
arrowPaint,
);
canvas.drawLine(
Offset(handlePos.dx, handlePos.dy - 4),
Offset(handlePos.dx + 4, handlePos.dy - 8),
arrowPaint,
);
// Down arrow
canvas.drawLine(
Offset(handlePos.dx, handlePos.dy + 4),
Offset(handlePos.dx - 4, handlePos.dy + 8),
arrowPaint,
);
canvas.drawLine(
Offset(handlePos.dx, handlePos.dy + 4),
Offset(handlePos.dx + 4, handlePos.dy + 8),
arrowPaint,
);
// Label
final textPainter = TextPainter(
text: const TextSpan(
text: 'INT.',
style: TextStyle(
color: Colors.white,
fontSize: 8,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(handlePos.dx - textPainter.width / 2, handlePos.dy - 24),
);
}
void _drawInstructions(Canvas canvas, Size size) { void _drawInstructions(Canvas canvas, Size size) {
const instruction = 'Deplacez le centre ou ajustez le rayon'; const instruction = 'Deplacez le centre ou ajustez le rayon';
@@ -630,7 +572,8 @@ class _CalibrationPainter extends CustomPainter {
} }
@override @override
bool shouldRepaint(covariant _CalibrationPainter oldDelegate) { bool shouldRepaint(CustomPainter oldDelegate) {
if (oldDelegate is! _CalibrationPainter) return true;
return centerX != oldDelegate.centerX || return centerX != oldDelegate.centerX ||
centerY != oldDelegate.centerY || centerY != oldDelegate.centerY ||
radius != oldDelegate.radius || radius != oldDelegate.radius ||

View File

@@ -1,18 +1,16 @@
/// É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 'package:provider/provider.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';
import '../crop/crop_screen.dart'; import '../crop/crop_screen.dart';
import '../session/session_provider.dart';
import 'widgets/image_source_button.dart'; import 'widgets/image_source_button.dart';
class CaptureScreen extends StatefulWidget { class CaptureScreen extends StatefulWidget {
@@ -24,14 +22,73 @@ 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) {
final sessionProvider = context.watch<SessionProvider>();
final title = sessionProvider.isSessionActive
? 'Cible ${sessionProvider.targetCount + 1}'
: 'Source';
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Nouvelle Analyse')), appBar: AppBar(title: Text(title)),
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Column( child: Column(
@@ -39,7 +96,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 +112,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(
@@ -73,21 +126,11 @@ class _CaptureScreenState extends State<CaptureScreen> {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
) )
else if (_selectedImagePath != null) else
_buildImagePreview(), _buildGuide(),
// Guide text
if (_selectedImagePath == null && !_isLoading) _buildGuide(),
], ],
), ),
), ),
floatingActionButton: _selectedImagePath != null
? FloatingActionButton.extended(
onPressed: _analyzeImage,
icon: const Icon(Icons.arrow_forward),
label: const Text('Suivant'),
)
: null,
); );
} }
@@ -100,65 +143,9 @@ class _CaptureScreenState extends State<CaptureScreen> {
); );
} }
Widget _buildImagePreview() {
return Column(
children: [
_buildSectionTitle('Apercu'),
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
child: Stack(
children: [
Image.file(
File(_selectedImagePath!),
fit: BoxFit.contain,
width: double.infinity,
),
Positioned(
top: 8,
right: 8,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() => _selectedImagePath = null);
},
style: IconButton.styleFrom(
backgroundColor: Colors.black54,
foregroundColor: Colors.white,
),
),
),
],
),
),
const SizedBox(height: 12),
_buildFramingHints(),
],
);
}
Widget _buildFramingHints() {
return Card(
color: AppTheme.warningColor.withValues(alpha: 0.1),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Icon(Icons.info_outline, color: AppTheme.warningColor),
const SizedBox(width: 12),
Expanded(
child: Text(
'Assurez-vous que la cible est bien centree et visible.',
style: TextStyle(
color: AppTheme.warningColor.withValues(alpha: 0.8),
),
),
),
],
),
),
);
}
Widget _buildGuide() { Widget _buildGuide() {
return Card( return Card(
@@ -177,11 +164,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 +194,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 +201,21 @@ 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); _selectedImagePath = documents.images.first;
_analyzeImage();
} }
} 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 +223,19 @@ class _CaptureScreenState extends State<CaptureScreen> {
maxHeight: 2048, maxHeight: 2048,
imageQuality: 90, imageQuality: 90,
); );
if (image != null) { if (image != null) {
setState(() => _selectedImagePath = image.path); _selectedImagePath = image.path;
_analyzeImage();
} }
} catch (e) { } catch (e) {
if (mounted) { debugPrint('Erreur capture: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la capture: $e'),
backgroundColor: AppTheme.errorColor,
),
);
}
} finally { } finally {
if (mounted) { if (mounted) setState(() => _isLoading = false);
setState(() => _isLoading = false);
}
} }
} }
void _analyzeImage() { void _analyzeImage() {
if (_selectedImagePath == null) return; if (_selectedImagePath == null) return;
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(

View File

@@ -1,13 +1,7 @@
/// É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:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
import '../../services/image_crop_service.dart'; import '../../services/image_crop_service.dart';
@@ -17,11 +11,15 @@ import 'widgets/crop_overlay.dart';
class CropScreen extends StatefulWidget { class CropScreen extends StatefulWidget {
final String imagePath; final String imagePath;
final TargetType targetType; final TargetType targetType;
final double? initialScale;
final Offset? initialOffset;
const CropScreen({ const CropScreen({
super.key, super.key,
required this.imagePath, required this.imagePath,
required this.targetType, required this.targetType,
this.initialScale,
this.initialOffset,
}); });
@override @override
@@ -31,98 +29,145 @@ class CropScreen extends StatefulWidget {
class _CropScreenState extends State<CropScreen> { class _CropScreenState extends State<CropScreen> {
final ImageCropService _cropService = ImageCropService(); final ImageCropService _cropService = ImageCropService();
// États de transformation
double _scale = 1.0;
double _baseScale = 1.0;
Offset _offset = Offset.zero;
Offset _startOffset = Offset.zero;
Offset _startFocalPoint = Offset.zero;
bool _isLoading = false; bool _isLoading = false;
bool _imageLoaded = false; bool _imageLoaded = false;
Size? _imageSize; Size? _imageSize;
late Size _viewportSize;
// Position et échelle de l'image late double _cropSize;
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 @override
void initState() { void initState() {
super.initState(); super.initState();
_loadImageDimensions(); _loadImageInfo();
} }
Future<void> _loadImageDimensions() async { Future<void> _loadImageInfo() async {
final file = File(widget.imagePath); final file = File(widget.imagePath);
final bytes = await file.readAsBytes(); final decodedImage = await decodeImageFromList(await file.readAsBytes());
final codec = await ui.instantiateImageCodec(bytes); if (mounted) {
final frame = await codec.getNextFrame(); setState(() {
_imageSize = Size(
setState(() { decodedImage.width.toDouble(),
_imageSize = Size( decodedImage.height.toDouble(),
frame.image.width.toDouble(), );
frame.image.height.toDouble(), _imageLoaded = true;
); });
_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(
'Centrage de la cible',
style: TextStyle(color: Colors.white, fontSize: 18),
),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
actions: [
if (!_isLoading)
IconButton(
icon: const Icon(Icons.check),
onPressed: _onCropConfirm,
),
],
), ),
body: _buildBody(), body: _isLoading
? const Center(child: CircularProgressIndicator(color: Color(0xFF1A73E8)))
: Column(
children: [
// Zone interactive de crop
Expanded(
child: Container(
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white10),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: _imageLoaded ? _buildInteractiveCrop() : const Center(child: CircularProgressIndicator()),
),
),
),
// Bouton d'aide / Modifier
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.center_focus_strong, color: Colors.redAccent, size: 20),
const SizedBox(width: 8),
Text(
'Alignez le centre de la cible sur la croix',
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 4),
Text(
'Zoomez pour plus de précision',
style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12),
),
],
),
),
// Boutons du bas
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 30),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.white24),
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Retour', style: TextStyle(color: Colors.white)),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _onCropConfirm,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1A73E8),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('Analyser', style: TextStyle(fontWeight: FontWeight.bold)),
),
),
],
),
),
],
),
); );
} }
Widget _buildBody() { Widget _buildInteractiveCrop() {
if (!_imageLoaded || _imageSize == null) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
if (_isLoading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(color: Colors.white),
const SizedBox(height: 16),
Text(
'Recadrage en cours...',
style: TextStyle(color: Colors.white.withValues(alpha: 0.8)),
),
],
),
);
}
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
_viewportSize = Size(constraints.maxWidth, constraints.maxHeight); _viewportSize = Size(constraints.maxWidth, constraints.maxHeight);
// Taille du carré de crop (90% de la plus petite dimension) // Le carré de crop occupe presque tout l'espace disponible
_cropSize = _cropSize = math.min(constraints.maxWidth, constraints.maxHeight) * 0.95;
math.min(constraints.maxWidth, constraints.maxHeight) * 0.85;
// Calculer l'échelle initiale si pas encore fait
if (_scale == 1.0 && _offset == Offset.zero) { if (_scale == 1.0 && _offset == Offset.zero) {
_initializeImagePosition(); _initializeImagePosition();
} }
@@ -130,10 +175,8 @@ class _CropScreenState extends State<CropScreen> {
return GestureDetector( return GestureDetector(
onScaleStart: _onScaleStart, onScaleStart: _onScaleStart,
onScaleUpdate: _onScaleUpdate, onScaleUpdate: _onScaleUpdate,
onScaleEnd: _onScaleEnd,
child: Stack( child: Stack(
children: [ children: [
// Image transformée
Positioned.fill( Positioned.fill(
child: Center( child: Center(
child: Transform( child: Transform(
@@ -150,21 +193,11 @@ class _CropScreenState extends State<CropScreen> {
), ),
), ),
), ),
// Overlay de recadrage
Positioned.fill( Positioned.fill(
child: IgnorePointer( child: IgnorePointer(
child: CropOverlay(cropSize: _cropSize, showGrid: true), child: CropOverlay(cropSize: _cropSize, showGrid: false),
), ),
), ),
// Instructions en bas
Positioned(
left: 0,
right: 0,
bottom: 24,
child: _buildInstructions(),
),
], ],
), ),
); );
@@ -178,7 +211,6 @@ class _CropScreenState extends State<CropScreen> {
final imageAspect = _imageSize!.width / _imageSize!.height; final imageAspect = _imageSize!.width / _imageSize!.height;
final viewportAspect = _viewportSize.width / _viewportSize.height; final viewportAspect = _viewportSize.width / _viewportSize.height;
// Calculer la taille de l'image affichée (avec BoxFit.contain)
double displayWidth, displayHeight; double displayWidth, displayHeight;
if (imageAspect > viewportAspect) { if (imageAspect > viewportAspect) {
displayWidth = _viewportSize.width; displayWidth = _viewportSize.width;
@@ -188,12 +220,14 @@ class _CropScreenState extends State<CropScreen> {
displayWidth = _viewportSize.height * imageAspect; 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); final minDisplayDim = math.min(displayWidth, displayHeight);
_scale = _cropSize / minDisplayDim;
// S'assurer d'un scale minimum _scale = widget.initialScale ?? (_cropSize / minDisplayDim);
if (_scale < 1.0) _scale = 1.0; if (_scale < 1.0 && widget.initialScale == null) _scale = 1.0;
if (widget.initialOffset != null) {
_offset = widget.initialOffset!;
}
} }
void _onScaleStart(ScaleStartDetails details) { void _onScaleStart(ScaleStartDetails details) {
@@ -204,102 +238,49 @@ class _CropScreenState extends State<CropScreen> {
void _onScaleUpdate(ScaleUpdateDetails details) { void _onScaleUpdate(ScaleUpdateDetails details) {
setState(() { setState(() {
// Mise à jour du scale _scale = (_baseScale * details.scale).clamp(0.8, 8.0);
_scale = (_baseScale * details.scale).clamp(0.5, 5.0);
// Mise à jour de la position
final delta = details.focalPoint - _startFocalPoint; final delta = details.focalPoint - _startFocalPoint;
_offset = _startOffset + delta; _offset = _startOffset + delta;
}); });
} }
void _onScaleEnd(ScaleEndDetails details) {
// Optionnel: contraindre l'image pour qu'elle couvre toujours le carré de crop
}
Widget _buildInstructions() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.touch_app,
color: Colors.white.withValues(alpha: 0.8),
size: 20,
),
const SizedBox(width: 8),
Text(
'Déplacez et zoomez pour cadrer la cible',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 14,
),
),
],
),
);
}
Future<void> _onCropConfirm() async { Future<void> _onCropConfirm() async {
if (_imageSize == null) return; setState(() => _isLoading = true);
setState(() {
_isLoading = true;
});
try { try {
// Calculer la zone de crop en coordonnées normalisées de l'image
final cropRect = _calculateCropRect(); final cropRect = _calculateCropRect();
// On calcule le centre relatif basé sur le centrage utilisateur
// Recadrer l'image final targetCenterX = cropRect.x + cropRect.width / 2;
final croppedPath = await _cropService.cropToSquare( final targetCenterY = cropRect.y + cropRect.height / 2;
widget.imagePath,
cropRect,
);
if (!mounted) return; if (!mounted) return;
// Naviguer vers l'écran d'analyse avec l'image recadrée
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => AnalysisScreen( builder: (_) => AnalysisScreen(
imagePath: croppedPath, imagePath: widget.imagePath,
targetType: widget.targetType, targetType: widget.targetType,
targetCenter: Offset(targetCenterX, targetCenterY),
cropScale: _scale,
cropOffset: _offset,
), ),
), ),
); );
} catch (e) { } catch (e) {
if (!mounted) return; if (mounted) {
setState(() => _isLoading = false);
setState(() { ScaffoldMessenger.of(context).showSnackBar(
_isLoading = false; SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor),
}); );
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors du recadrage: $e'),
backgroundColor: AppTheme.errorColor,
),
);
} }
} }
CropRect _calculateCropRect() { CropRect _calculateCropRect() {
if (_imageSize == null) { if (_imageSize == null) return const CropRect(x: 0, y: 0, width: 1, height: 1);
return const CropRect(x: 0, y: 0, width: 1, height: 1);
}
final imageAspect = _imageSize!.width / _imageSize!.height; final imageAspect = _imageSize!.width / _imageSize!.height;
final viewportAspect = _viewportSize.width / _viewportSize.height; final viewportAspect = _viewportSize.width / _viewportSize.height;
// Calculer la taille de l'image affichée (avec BoxFit.contain)
double displayWidth, displayHeight; double displayWidth, displayHeight;
if (imageAspect > viewportAspect) { if (imageAspect > viewportAspect) {
displayWidth = _viewportSize.width; displayWidth = _viewportSize.width;
@@ -309,33 +290,28 @@ class _CropScreenState extends State<CropScreen> {
displayWidth = _viewportSize.height * imageAspect; displayWidth = _viewportSize.height * imageAspect;
} }
// Taille de l'image après scale
final scaledWidth = displayWidth * _scale; final scaledWidth = displayWidth * _scale;
final scaledHeight = displayHeight * _scale; final scaledHeight = displayHeight * _scale;
// Position du centre de l'image dans le viewport
final imageCenterX = _viewportSize.width / 2 + _offset.dx; final imageCenterX = _viewportSize.width / 2 + _offset.dx;
final imageCenterY = _viewportSize.height / 2 + _offset.dy; final imageCenterY = _viewportSize.height / 2 + _offset.dy;
// Position du coin supérieur gauche de l'image
final imageLeft = imageCenterX - scaledWidth / 2; final imageLeft = imageCenterX - scaledWidth / 2;
final imageTop = imageCenterY - scaledHeight / 2; final imageTop = imageCenterY - scaledHeight / 2;
// Position du carré de crop (centré dans le viewport)
final cropLeft = (_viewportSize.width - _cropSize) / 2; final cropLeft = (_viewportSize.width - _cropSize) / 2;
final cropTop = (_viewportSize.height - _cropSize) / 2; final cropTop = (_viewportSize.height - _cropSize) / 2;
// Convertir en coordonnées relatives à l'image affichée
final relCropLeft = (cropLeft - imageLeft) / scaledWidth; final relCropLeft = (cropLeft - imageLeft) / scaledWidth;
final relCropTop = (cropTop - imageTop) / scaledHeight; final relCropTop = (cropTop - imageTop) / scaledHeight;
final relCropSize = _cropSize / scaledWidth; final relCropWidth = _cropSize / scaledWidth;
final relCropSizeY = _cropSize / scaledHeight; final relCropHeight = _cropSize / scaledHeight;
return CropRect( return CropRect(
x: relCropLeft.clamp(0.0, 1.0), x: relCropLeft,
y: relCropTop.clamp(0.0, 1.0), y: relCropTop,
width: relCropSize.clamp(0.0, 1.0 - relCropLeft.clamp(0.0, 1.0)), width: relCropWidth,
height: relCropSizeY.clamp(0.0, 1.0 - relCropTop.clamp(0.0, 1.0)), height: relCropHeight,
); );
} }
} }

View File

@@ -0,0 +1,414 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../core/constants/app_constants.dart';
import '../../core/theme/app_theme.dart';
import '../../data/models/weapon.dart';
import '../../data/models/maintenance.dart';
import '../../data/repositories/session_repository.dart';
class WeaponDetailScreen extends StatefulWidget {
final Weapon weapon;
const WeaponDetailScreen({super.key, required this.weapon});
@override
State<WeaponDetailScreen> createState() => _WeaponDetailScreenState();
}
class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
late Weapon _weapon;
List<MaintenanceEntry> _maintenance = [];
int _totalRounds = 0;
bool _isLoading = true;
@override
void initState() {
super.initState();
_weapon = widget.weapon;
_loadData();
}
Future<void> _loadData() async {
final repository = context.read<SessionRepository>();
final history = await repository.getMaintenanceHistory(_weapon.id);
final rounds = await repository.getRoundsFiredForWeapon(_weapon.id);
if (mounted) {
setState(() {
_maintenance = history;
_totalRounds = rounds;
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_weapon.customName ?? _weapon.name),
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditWeaponDialog(context),
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildStatsSection(),
const SizedBox(height: 24),
_buildInfoCard(),
const SizedBox(height: 24),
_buildOptionsCard(),
const SizedBox(height: 24),
_buildHistorySection(),
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _showAddMaintenanceDialog(context),
label: const Text('Entretien'),
icon: const Icon(Icons.build),
),
);
}
Widget _buildStatsSection() {
return Row(
children: [
Expanded(
child: _buildStatCard(
'Total Tirs',
_totalRounds.toString(),
Icons.ads_click,
AppTheme.primaryColor,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatCard(
'Sessions',
'N/A',
Icons.history,
Colors.orange,
),
),
],
);
}
Widget _buildStatCard(String label, String value, IconData icon, Color color) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Icon(icon, color: color),
const SizedBox(height: 8),
Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
);
}
Widget _buildInfoCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Informations techniques', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const Divider(),
_buildInfoRow('Modèle', _weapon.name),
_buildInfoRow('Type', _weapon.type.displayName),
_buildInfoRow('Calibre', _weapon.caliber),
_buildInfoRow('Chargeurs', '${_weapon.magazineCount} x ${_weapon.magazineCapacity} coups'),
if (_weapon.notes != null && _weapon.notes!.isNotEmpty) ...[
const SizedBox(height: 8),
const Text('Notes:', style: TextStyle(fontWeight: FontWeight.w500)),
Text(_weapon.notes!, style: const TextStyle(color: Colors.grey)),
],
],
),
),
);
}
Widget _buildOptionsCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Options & Personnalisation', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const Divider(),
_buildInfoRow('Optique / Lunette', _weapon.optic ?? 'Mire fer'),
_buildInfoRow('Modérateur / Silencieux', _weapon.silencer ?? 'Aucun'),
_buildInfoRow('Détente', _weapon.trigger ?? 'Standard'),
if (_weapon.customName != null)
_buildInfoRow('Surnom', _weapon.customName!),
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(color: Colors.grey)),
Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
],
),
);
}
Widget _buildHistorySection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Historique d\'entretien', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
const SizedBox(height: 8),
if (_maintenance.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 24.0),
child: Center(child: Text('Aucun entretien enregistré', style: TextStyle(color: Colors.grey))),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _maintenance.length,
itemBuilder: (context, index) {
final entry = _maintenance[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: _getMaintenanceColor(entry.type).withValues(alpha: 0.1),
child: Icon(_getMaintenanceIcon(entry.type), color: _getMaintenanceColor(entry.type), size: 20),
),
title: Text(entry.type.displayName),
subtitle: Text(entry.description),
trailing: Text(DateFormat('dd/MM/yy').format(entry.date), style: const TextStyle(fontSize: 12)),
onLongPress: () => _confirmDeleteMaintenance(context, entry),
),
);
},
),
],
);
}
Color _getMaintenanceColor(MaintenanceType type) {
switch (type) {
case MaintenanceType.cleaning: return Colors.blue;
case MaintenanceType.repair: return Colors.red;
case MaintenanceType.inspection: return Colors.green;
case MaintenanceType.upgrade: return Colors.purple;
case MaintenanceType.other: return Colors.grey;
}
}
IconData _getMaintenanceIcon(MaintenanceType type) {
switch (type) {
case MaintenanceType.cleaning: return Icons.opacity;
case MaintenanceType.repair: return Icons.build;
case MaintenanceType.inspection: return Icons.visibility;
case MaintenanceType.upgrade: return Icons.trending_up;
case MaintenanceType.other: return Icons.more_horiz;
}
}
void _showEditWeaponDialog(BuildContext context) async {
final nameController = TextEditingController(text: _weapon.name);
final caliberController = TextEditingController(text: _weapon.caliber);
final magCountController = TextEditingController(text: _weapon.magazineCount.toString());
final magCapController = TextEditingController(text: _weapon.magazineCapacity.toString());
final notesController = TextEditingController(text: _weapon.notes);
final opticController = TextEditingController(text: _weapon.optic);
final silencerController = TextEditingController(text: _weapon.silencer);
final triggerController = TextEditingController(text: _weapon.trigger);
final customNameController = TextEditingController(text: _weapon.customName);
WeaponType selectedType = _weapon.type;
final result = await showDialog<bool>(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('Modifier l\'arme'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(labelText: 'Modèle', hintText: 'ex: Glock 17'),
),
TextField(
controller: customNameController,
decoration: const InputDecoration(labelText: 'Surnom / Custom Name', hintText: 'ex: Mon Glock de Compète'),
),
DropdownButtonFormField<WeaponType>(
value: selectedType,
decoration: const InputDecoration(labelText: 'Type'),
items: WeaponType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
onChanged: (v) => setState(() => selectedType = v!),
),
TextField(
controller: caliberController,
decoration: const InputDecoration(labelText: 'Calibre'),
),
const SizedBox(height: 16),
const Text('Équipement & Options', style: TextStyle(fontWeight: FontWeight.bold)),
TextField(
controller: opticController,
decoration: const InputDecoration(labelText: 'Optique / Lunette', hintText: 'ex: Holosun 507C'),
),
TextField(
controller: silencerController,
decoration: const InputDecoration(labelText: 'Modérateur / Silencieux'),
),
TextField(
controller: triggerController,
decoration: const InputDecoration(labelText: 'Détente / Trigger'),
),
const SizedBox(height: 16),
const Text('Logistique', style: TextStyle(fontWeight: FontWeight.bold)),
Row(
children: [
Expanded(
child: TextField(
controller: magCountController,
decoration: const InputDecoration(labelText: 'Chargeurs'),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: magCapController,
decoration: const InputDecoration(labelText: 'Capacité'),
keyboardType: TextInputType.number,
),
),
],
),
TextField(
controller: notesController,
decoration: const InputDecoration(labelText: 'Notes'),
maxLines: 2,
),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Annuler')),
ElevatedButton(onPressed: () => Navigator.pop(context, true), child: const Text('Enregistrer')),
],
),
),
);
if (result == true) {
final updatedWeapon = _weapon.copyWith(
name: nameController.text,
type: selectedType,
caliber: caliberController.text,
magazineCount: int.tryParse(magCountController.text) ?? 1,
magazineCapacity: int.tryParse(magCapController.text) ?? 10,
notes: notesController.text,
optic: opticController.text.isEmpty ? null : opticController.text,
silencer: silencerController.text.isEmpty ? null : silencerController.text,
trigger: triggerController.text.isEmpty ? null : triggerController.text,
customName: customNameController.text.isEmpty ? null : customNameController.text,
);
final repository = context.read<SessionRepository>();
await repository.updateWeapon(updatedWeapon);
setState(() {
_weapon = updatedWeapon;
});
}
}
void _showAddMaintenanceDialog(BuildContext context) async {
final descController = TextEditingController();
MaintenanceType selectedType = MaintenanceType.cleaning;
final result = await showDialog<bool>(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('Ajouter un entretien'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
DropdownButtonFormField<MaintenanceType>(
value: selectedType,
decoration: const InputDecoration(labelText: 'Type d\'intervention'),
items: MaintenanceType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
onChanged: (v) => setState(() => selectedType = v!),
),
TextField(
controller: descController,
decoration: const InputDecoration(labelText: 'Description', hintText: 'ex: Nettoyage complet après séance'),
maxLines: 2,
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Annuler')),
ElevatedButton(onPressed: () => Navigator.pop(context, true), child: const Text('Ajouter')),
],
),
),
);
if (result == true && descController.text.isNotEmpty) {
final repository = context.read<SessionRepository>();
await repository.addMaintenanceEntry(
weaponId: _weapon.id,
type: selectedType,
description: descController.text,
roundsSinceLast: _totalRounds,
);
_loadData();
}
}
void _confirmDeleteMaintenance(BuildContext context, MaintenanceEntry entry) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Supprimer'),
content: const Text('Supprimer cette entrée de l\'historique ?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Annuler')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Supprimer', style: TextStyle(color: AppTheme.errorColor)),
),
],
),
);
if (confirmed == true) {
final repository = context.read<SessionRepository>();
await repository.deleteMaintenanceEntry(entry.id);
_loadData();
}
}
}

View File

@@ -0,0 +1,208 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../core/constants/app_constants.dart';
import '../../core/theme/app_theme.dart';
import '../../data/models/weapon.dart';
import '../../data/repositories/session_repository.dart';
import 'weapon_detail_screen.dart';
class WeaponListScreen extends StatefulWidget {
const WeaponListScreen({super.key});
@override
State<WeaponListScreen> createState() => _WeaponListScreenState();
}
class _WeaponListScreenState extends State<WeaponListScreen> {
List<Weapon> _weapons = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadWeapons();
}
Future<void> _loadWeapons() async {
final repository = context.read<SessionRepository>();
final weapons = await repository.getWeapons();
if (mounted) {
setState(() {
_weapons = weapons;
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Mon Armurerie'),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _weapons.isEmpty
? _buildEmptyState()
: _buildWeaponList(),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddWeaponDialog(context),
child: const Icon(Icons.add),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.shield, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
const Text('Aucune arme enregistrée'),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => _showAddWeaponDialog(context),
child: const Text('Ajouter ma première arme'),
),
],
),
);
}
Widget _buildWeaponList() {
return ListView.builder(
padding: const EdgeInsets.all(AppConstants.defaultPadding),
itemCount: _weapons.length,
itemBuilder: (context, index) {
final weapon = _weapons[index];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(
backgroundColor: AppTheme.primaryColor.withValues(alpha: 0.1),
child: Icon(
weapon.type == WeaponType.handgun ? Icons.shield : Icons.ads_click,
color: AppTheme.primaryColor,
),
),
title: Text(weapon.displayName, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text('${weapon.type.displayName}${weapon.caliber}'),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('${weapon.magazineCount} chargeurs', style: const TextStyle(fontSize: 12)),
Text('${weapon.magazineCapacity} coups', style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(builder: (_) => WeaponDetailScreen(weapon: weapon)),
);
_loadWeapons(); // Reload in case it was edited or maintenance was added
},
onLongPress: () => _confirmDelete(context, weapon),
),
);
},
);
}
void _showAddWeaponDialog(BuildContext context) async {
final nameController = TextEditingController();
final caliberController = TextEditingController();
final magCountController = TextEditingController(text: '2');
final magCapController = TextEditingController(text: '15');
WeaponType selectedType = WeaponType.handgun;
final result = await showDialog<bool>(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('Ajouter une arme'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(labelText: 'Nom de l\'arme', hintText: 'ex: Glock 17'),
),
DropdownButtonFormField<WeaponType>(
value: selectedType,
decoration: const InputDecoration(labelText: 'Type'),
items: WeaponType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
onChanged: (v) => setState(() => selectedType = v!),
),
TextField(
controller: caliberController,
decoration: const InputDecoration(labelText: 'Calibre', hintText: 'ex: 9mm, .22LR'),
),
Row(
children: [
Expanded(
child: TextField(
controller: magCountController,
decoration: const InputDecoration(labelText: 'Chargeurs'),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: magCapController,
decoration: const InputDecoration(labelText: 'Capacité'),
keyboardType: TextInputType.number,
),
),
],
),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Annuler')),
ElevatedButton(onPressed: () => Navigator.pop(context, true), child: const Text('Ajouter')),
],
),
),
);
if (result == true && nameController.text.isNotEmpty) {
final repository = context.read<SessionRepository>();
await repository.addWeapon(
name: nameController.text,
type: selectedType,
caliber: caliberController.text,
magazineCount: int.tryParse(magCountController.text) ?? 1,
magazineCapacity: int.tryParse(magCapController.text) ?? 10,
);
_loadWeapons();
}
}
void _confirmDelete(BuildContext context, Weapon weapon) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Supprimer'),
content: Text('Voulez-vous supprimer ${weapon.name} de votre armurerie ?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Annuler')),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Supprimer', style: TextStyle(color: AppTheme.errorColor)),
),
],
),
);
if (confirmed == true) {
final repository = context.read<SessionRepository>();
await repository.deleteWeapon(weapon.id);
_loadWeapons();
}
}
}

View File

@@ -1,10 +1,3 @@
/// Écran d'historique des sessions.
///
/// Affiche la liste des sessions passées avec filtrage par type de cible.
/// Inclut un graphique d'évolution des scores (10 dernières sessions)
/// et permet la navigation vers le détail de chaque session.
library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@@ -29,6 +22,9 @@ class _HistoryScreenState extends State<HistoryScreen> {
bool _isLoading = true; bool _isLoading = true;
TargetType? _filterType; TargetType? _filterType;
// --- MODIFICATION : Remplacement de DateTime par DateTimeRange ---
DateTimeRange? _selectedDateRange;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -36,17 +32,43 @@ class _HistoryScreenState extends State<HistoryScreen> {
} }
Future<void> _loadSessions() async { Future<void> _loadSessions() async {
if (!mounted) return;
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
final repository = context.read<SessionRepository>(); final repository = context.read<SessionRepository>();
final sessions = await repository.getAllSessions( final sessions = await repository.getAllSessions();
targetType: _filterType,
);
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,40 @@ 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: ThemeData.light().copyWith(
colorScheme: ColorScheme.light(
primary: AppTheme.primaryColor, // En-tête et sélection
onPrimary: Colors.white, // Texte sur en-tête/sélection
surface: Colors.white, // Fond du calendrier
onSurface: Colors.black87, // Texte des dates (Noir sur Blanc)
secondary: AppTheme.primaryColor,
),
dialogBackgroundColor: Colors.white,
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: AppTheme.primaryColor),
),
),
child: child!,
);
},
);
if (picked != null) {
setState(() => _selectedDateRange = picked);
_loadSessions();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -71,55 +127,86 @@ class _HistoryScreenState extends State<HistoryScreen> {
actions: [ actions: [
PopupMenuButton<TargetType?>( PopupMenuButton<TargetType?>(
icon: const Icon(Icons.filter_list), icon: const Icon(Icons.filter_list),
tooltip: 'Filtrer',
onSelected: (type) { onSelected: (type) {
setState(() => _filterType = type); setState(() => _filterType = type);
_loadSessions(); _loadSessions();
}, },
itemBuilder: (context) => [ itemBuilder: (context) => [
const PopupMenuItem( const PopupMenuItem(value: null, child: Text('Tous')),
value: null, ...TargetType.values.map(
child: Text('Tous'), (type) =>
PopupMenuItem(value: type, child: Text(type.displayName)),
), ),
...TargetType.values.map((type) => PopupMenuItem(
value: type,
child: Text(type.displayName),
)),
], ],
), ),
], ],
), ),
body: _isLoading body: Column(
? const Center(child: CircularProgressIndicator()) children: [
: _sessions.isEmpty Expanded(
? _buildEmptyState() child: _isLoading
: _buildContent(), ? const Center(child: CircularProgressIndicator())
: _sessions.isEmpty
? _buildEmptyState()
: _buildContent(),
),
_buildBottomFilterBar(),
],
),
);
}
Widget _buildBottomFilterBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _pickDateRange,
icon: const Icon(Icons.date_range, size: 18),
label: Text(
_selectedDateRange == null
? 'Choisir une période'
: '${DateFormat('dd/MM/yy').format(_selectedDateRange!.start)} - ${DateFormat('dd/MM/yy').format(_selectedDateRange!.end)}',
),
),
),
if (_selectedDateRange != null)
IconButton(
icon: const Icon(Icons.close, color: AppTheme.errorColor),
onPressed: () {
setState(() => _selectedDateRange = null);
_loadSessions();
},
),
],
),
),
); );
} }
Widget _buildEmptyState() { Widget _buildEmptyState() {
return Center( return Center(
child: Padding( child: Column(
padding: const EdgeInsets.all(AppConstants.defaultPadding), mainAxisAlignment: MainAxisAlignment.center,
child: Column( children: [
mainAxisAlignment: MainAxisAlignment.center, Icon(Icons.history, size: 64, color: Colors.grey[400]),
children: [ const SizedBox(height: 16),
Icon(Icons.history, size: 64, color: Colors.grey[400]), const Text('Aucune session sur cette période'),
const SizedBox(height: 16), ],
Text(
'Aucune session',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
_filterType != null
? 'Aucune session de type ${_filterType!.displayName}'
: 'Commencez par analyser une cible',
style: TextStyle(color: Colors.grey[600]),
textAlign: TextAlign.center,
),
],
),
), ),
); );
} }
@@ -129,49 +216,27 @@ class _HistoryScreenState extends State<HistoryScreen> {
onRefresh: _loadSessions, onRefresh: _loadSessions,
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
// Chart section if (_sessions.length >= 2 && _selectedDateRange == null)
if (_sessions.length >= 2)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: HistoryChart(sessions: _sessions), child: HistoryChart(sessions: _sessions),
), ),
), ),
// Filter indicator
if (_filterType != null)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppConstants.defaultPadding),
child: Chip(
label: Text('Filtre: ${_filterType!.displayName}'),
deleteIcon: const Icon(Icons.close, size: 18),
onDeleted: () {
setState(() => _filterType = null);
_loadSessions();
},
),
),
),
// Sessions list
SliverPadding( SliverPadding(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(AppConstants.defaultPadding),
sliver: SliverList( sliver: SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate((context, index) {
(context, index) { final session = _sessions[index];
final session = _sessions[index]; return Padding(
return Padding( padding: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.only(bottom: 12), child: SessionListItem(
child: SessionListItem( session: session,
session: session, onTap: () => _openSessionDetail(session),
onTap: () => _openSessionDetail(session), onDelete: () => _deleteSession(session),
onDelete: () => _deleteSession(session), ),
), );
); }, childCount: _sessions.length),
},
childCount: _sessions.length,
),
), ),
), ),
], ],
@@ -182,11 +247,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 +267,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 +281,8 @@ class _HistoryScreenState extends State<HistoryScreen> {
final repository = context.read<SessionRepository>(); final repository = context.read<SessionRepository>();
await repository.deleteSession(session.id); await repository.deleteSession(session.id);
_loadSessions(); _loadSessions();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Session supprimee')),
);
}
} catch (e) { } catch (e) {
if (mounted) { debugPrint('Erreur suppression: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: $e'),
backgroundColor: AppTheme.errorColor,
),
);
}
} }
} }
} }

View File

@@ -1,10 +1,3 @@
/// Écran de détail d'une session.
///
/// Affiche la visualisation complète d'une session sauvegardée :
/// image de la cible avec overlay des impacts, scores recalculés,
/// statistiques de groupement et lien vers les statistiques détaillées.
library;
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -12,6 +5,8 @@ import 'package:intl/intl.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/session.dart'; import '../../data/models/session.dart';
import '../../data/models/target_analysis.dart';
import '../../data/models/target_type.dart';
import '../../data/repositories/session_repository.dart'; import '../../data/repositories/session_repository.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';
@@ -20,7 +15,7 @@ import '../analysis/widgets/score_card.dart';
import '../analysis/widgets/grouping_stats.dart'; import '../analysis/widgets/grouping_stats.dart';
import '../statistics/statistics_screen.dart'; import '../statistics/statistics_screen.dart';
class SessionDetailScreen extends StatelessWidget { class SessionDetailScreen extends StatefulWidget {
final Session session; final Session session;
const SessionDetailScreen({ const SessionDetailScreen({
@@ -28,33 +23,40 @@ class SessionDetailScreen extends StatelessWidget {
required this.session, required this.session,
}); });
@override
State<SessionDetailScreen> createState() => _SessionDetailScreenState();
}
class _SessionDetailScreenState extends State<SessionDetailScreen> {
int _currentTargetIndex = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scoreCalculator = context.read<ScoreCalculatorService>(); final scoreCalculator = context.read<ScoreCalculatorService>();
final groupingAnalyzer = context.read<GroupingAnalyzerService>(); final groupingAnalyzer = context.read<GroupingAnalyzerService>();
final currentAnalysis = widget.session.analyses[_currentTargetIndex];
final scoreResult = scoreCalculator.calculateScores( final scoreResult = scoreCalculator.calculateScores(
shots: session.shots, shots: currentAnalysis.shots,
targetType: session.targetType, targetType: currentAnalysis.targetType,
targetCenterX: session.targetCenterX ?? 0.5, targetCenterX: currentAnalysis.targetCenterX ?? 0.5,
targetCenterY: session.targetCenterY ?? 0.5, targetCenterY: currentAnalysis.targetCenterY ?? 0.5,
targetRadius: session.targetRadius ?? 0.4, targetRadius: currentAnalysis.targetRadius ?? 0.4,
); );
final groupingResult = groupingAnalyzer.analyzeGrouping(session.shots); final groupingResult = groupingAnalyzer.analyzeGrouping(currentAnalysis.shots);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(widget.session.weapon),
DateFormat('dd/MM/yyyy HH:mm').format(session.createdAt),
),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.analytics), icon: const Icon(Icons.analytics),
onPressed: () => Navigator.push( onPressed: () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => StatisticsScreen(singleSession: session), builder: (_) => StatisticsScreen(singleSession: widget.session),
), ),
), ),
tooltip: 'Statistiques', tooltip: 'Statistiques',
@@ -70,15 +72,22 @@ class SessionDetailScreen extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Weapon and session overview
_buildSessionHeader(context),
// Target selector if multiple targets
if (widget.session.targetCount > 1)
_buildTargetSelector(),
// Target image with overlay // Target image with overlay
AspectRatio( AspectRatio(
aspectRatio: 1, aspectRatio: 1,
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
if (File(session.imagePath).existsSync()) if (File(currentAnalysis.imagePath).existsSync())
Image.file( Image.file(
File(session.imagePath), File(currentAnalysis.imagePath),
fit: BoxFit.contain, fit: BoxFit.contain,
) )
else else
@@ -89,14 +98,14 @@ class SessionDetailScreen extends StatelessWidget {
), ),
), ),
TargetOverlay( TargetOverlay(
shots: session.shots, shots: currentAnalysis.shots,
targetCenterX: session.targetCenterX ?? 0.5, targetCenterX: currentAnalysis.targetCenterX ?? 0.5,
targetCenterY: session.targetCenterY ?? 0.5, targetCenterY: currentAnalysis.targetCenterY ?? 0.5,
targetRadius: session.targetRadius ?? 0.4, targetRadius: currentAnalysis.targetRadius ?? 0.4,
targetType: session.targetType, targetType: currentAnalysis.targetType,
groupingCenterX: session.groupingCenterX, groupingCenterX: currentAnalysis.groupingCenterX,
groupingCenterY: session.groupingCenterY, groupingCenterY: currentAnalysis.groupingCenterY,
groupingDiameter: session.groupingDiameter, groupingDiameter: currentAnalysis.groupingDiameter,
), ),
], ],
), ),
@@ -107,31 +116,37 @@ class SessionDetailScreen extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Session info // Analysis Info
_buildSessionInfo(context), _buildAnalysisInfo(context, currentAnalysis),
const SizedBox(height: 12), const SizedBox(height: 12),
// Score card // Score card for current target
ScoreCard( ScoreCard(
totalScore: session.totalScore, totalScore: currentAnalysis.totalScore,
shotCount: session.shotCount, shotCount: currentAnalysis.shotCount,
scoreResult: scoreResult, scoreResult: scoreResult,
targetType: session.targetType, targetType: currentAnalysis.targetType,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Grouping stats // Grouping stats for current target
if (session.shotCount > 1) if (currentAnalysis.shotCount > 1)
GroupingStats( GroupingStats(
groupingResult: groupingResult, groupingResult: groupingResult,
targetCenterX: session.targetCenterX ?? 0.5, targetCenterX: currentAnalysis.targetCenterX ?? 0.5,
targetCenterY: session.targetCenterY ?? 0.5, targetCenterY: currentAnalysis.targetCenterY ?? 0.5,
), ),
// Notes // Notes for current target
if (session.notes != null && session.notes!.isNotEmpty) ...[ if (currentAnalysis.notes != null && currentAnalysis.notes!.isNotEmpty) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
_buildNotesCard(context), _buildNotesCard(context, currentAnalysis.notes!),
],
// Session notes
if (widget.session.notes != null && widget.session.notes!.isNotEmpty) ...[
const SizedBox(height: 12),
_buildNotesCard(context, widget.session.notes!, title: 'Notes de session'),
], ],
], ],
), ),
@@ -142,16 +157,83 @@ class SessionDetailScreen extends StatelessWidget {
); );
} }
Widget _buildSessionInfo(BuildContext context) { Widget _buildSessionHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
color: AppTheme.primaryColor.withValues(alpha: 0.1),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('EEEE dd MMMM yyyy', 'fr_FR').format(widget.session.createdAt),
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
DateFormat('HH:mm').format(widget.session.createdAt),
style: Theme.of(context).textTheme.bodySmall,
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'Score Total: ${widget.session.totalScore}',
style: const TextStyle(fontWeight: FontWeight.bold, color: AppTheme.primaryColor),
),
Text(
'${widget.session.totalShots} tirs au total',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
],
),
],
),
);
}
Widget _buildTargetSelector() {
return Container(
height: 50,
margin: const EdgeInsets.symmetric(vertical: 8),
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: widget.session.targetCount,
itemBuilder: (context, index) {
final isSelected = _currentTargetIndex == index;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text('Cible ${index + 1}'),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setState(() => _currentTargetIndex = index);
}
},
),
);
},
),
);
}
Widget _buildAnalysisInfo(BuildContext context, TargetAnalysis analysis) {
return Card( return Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(12),
child: Row( child: Row(
children: [ children: [
Icon( Icon(
session.targetType == session.targetType analysis.targetType == TargetType.concentric ? Icons.track_changes : Icons.person,
? Icons.track_changes
: Icons.person,
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -160,14 +242,11 @@ class SessionDetailScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
session.targetType.displayName, analysis.targetType.displayName,
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: const TextStyle(fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
),
), ),
Text( Text(
DateFormat('EEEE dd MMMM yyyy, HH:mm', 'fr_FR') 'Cible ${_currentTargetIndex + 1} sur ${widget.session.targetCount}',
.format(session.createdAt),
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
], ],
@@ -179,7 +258,7 @@ class SessionDetailScreen extends StatelessWidget {
); );
} }
Widget _buildNotesCard(BuildContext context) { Widget _buildNotesCard(BuildContext context, String notes, {String title = 'Notes'}) {
return Card( return Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding), padding: const EdgeInsets.all(AppConstants.defaultPadding),
@@ -188,18 +267,16 @@ class SessionDetailScreen extends StatelessWidget {
children: [ children: [
Row( Row(
children: [ children: [
const Icon(Icons.notes, color: AppTheme.primaryColor), const Icon(Icons.notes, color: AppTheme.primaryColor, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Notes', title,
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: const TextStyle(fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
),
), ),
], ],
), ),
const Divider(), const Divider(),
Text(session.notes!), Text(notes),
], ],
), ),
), ),
@@ -211,7 +288,7 @@ class SessionDetailScreen extends StatelessWidget {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Supprimer'), title: const Text('Supprimer'),
content: const Text('Voulez-vous vraiment supprimer cette session?'), content: const Text('Voulez-vous vraiment supprimer cette session entière ?'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context, false), onPressed: () => Navigator.pop(context, false),
@@ -231,20 +308,17 @@ class SessionDetailScreen extends StatelessWidget {
if (confirmed == true && context.mounted) { if (confirmed == true && context.mounted) {
try { try {
final repository = context.read<SessionRepository>(); final repository = context.read<SessionRepository>();
await repository.deleteSession(session.id); await repository.deleteSession(widget.session.id);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Session supprimee')), const SnackBar(content: Text('Session supprimée')),
); );
Navigator.pop(context); Navigator.pop(context);
} }
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor),
content: Text('Erreur: $e'),
backgroundColor: AppTheme.errorColor,
),
); );
} }
} }

View File

@@ -155,7 +155,7 @@ class HistoryChart extends StatelessWidget {
return touchedSpots.map((spot) { return touchedSpots.map((spot) {
final session = displaySessions[spot.x.toInt()]; final session = displaySessions[spot.x.toInt()];
return LineTooltipItem( return LineTooltipItem(
'Score: ${session.totalScore}\n${session.shotCount} tirs', 'Score: ${session.totalScore}\n${session.totalShots} tirs',
const TextStyle( const TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@@ -1,15 +1,8 @@
/// Item de liste représentant une session.
///
/// Affiche les informations clés d'une session (date, type de cible,
/// score, nombre de tirs) avec miniature de l'image et actions de suppression.
library;
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.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/models/target_type.dart';
class SessionListItem extends StatelessWidget { class SessionListItem extends StatelessWidget {
final Session session; final Session session;
@@ -33,7 +26,7 @@ class SessionListItem extends StatelessWidget {
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Row( child: Row(
children: [ children: [
// Thumbnail // Thumbnail (from first target)
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: SizedBox( child: SizedBox(
@@ -51,14 +44,14 @@ class SessionListItem extends StatelessWidget {
children: [ children: [
Row( Row(
children: [ children: [
Icon( const Icon(
_getTargetIcon(), Icons.shield,
size: 16, size: 16,
color: AppTheme.primaryColor, color: AppTheme.primaryColor,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
session.targetType.displayName, session.weapon,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -70,18 +63,19 @@ class SessionListItem extends StatelessWidget {
DateFormat('dd/MM/yyyy HH:mm').format(session.createdAt), DateFormat('dd/MM/yyyy HH:mm').format(session.createdAt),
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
if (session.notes != null && session.notes!.isNotEmpty) ...[ const SizedBox(height: 4),
const SizedBox(height: 2), Row(
Text( children: [
session.notes!, Icon(Icons.track_changes, size: 14, color: Colors.grey[600]),
style: Theme.of(context).textTheme.bodySmall?.copyWith( const SizedBox(width: 4),
color: Colors.grey[600], Text(
fontStyle: FontStyle.italic, '${session.targetCount} cible(s) • ${session.distance}m',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
), ),
maxLines: 1, ],
overflow: TextOverflow.ellipsis, ),
),
],
], ],
), ),
), ),
@@ -98,7 +92,7 @@ class SessionListItem extends StatelessWidget {
), ),
), ),
Text( Text(
'${session.shotCount} tirs', '${session.totalShots} tirs',
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
], ],
@@ -120,7 +114,9 @@ class SessionListItem extends StatelessWidget {
} }
Widget _buildThumbnail() { Widget _buildThumbnail() {
final file = File(session.imagePath); if (session.analyses.isEmpty) return _buildPlaceholder();
final file = File(session.analyses.first.imagePath);
if (file.existsSync()) { if (file.existsSync()) {
return Image.file( return Image.file(
@@ -137,18 +133,9 @@ class SessionListItem extends StatelessWidget {
return Container( return Container(
color: Colors.grey[200], color: Colors.grey[200],
child: Icon( child: Icon(
_getTargetIcon(), Icons.track_changes,
color: Colors.grey[400], color: Colors.grey[400],
), ),
); );
} }
IconData _getTargetIcon() {
switch (session.targetType) {
case TargetType.concentric:
return Icons.track_changes;
case TargetType.silhouette:
return Icons.person;
}
}
} }

View File

@@ -1,9 +1,3 @@
/// Écran d'accueil - Dashboard principal de l'application.
///
/// Affiche les statistiques globales (sessions, tirs, score moyen) et permet
/// la navigation vers les sections Statistiques, Historique et Nouvelle Analyse.
library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../core/constants/app_constants.dart'; import '../../core/constants/app_constants.dart';
@@ -12,7 +6,11 @@ import '../../data/repositories/session_repository.dart';
import '../capture/capture_screen.dart'; import '../capture/capture_screen.dart';
import '../history/history_screen.dart'; import '../history/history_screen.dart';
import '../statistics/statistics_screen.dart'; import '../statistics/statistics_screen.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../data/models/session.dart';
import '../settings/settings_screen.dart'; import '../settings/settings_screen.dart';
import '../session/session_setup_screen.dart';
import '../session/session_provider.dart';
import 'widgets/stats_card.dart'; import 'widgets/stats_card.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@@ -24,6 +22,7 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> { class _HomeScreenState extends State<HomeScreen> {
Map<String, dynamic>? _stats; Map<String, dynamic>? _stats;
List<Session> _recentSessions = [];
bool _isLoading = true; bool _isLoading = true;
@override @override
@@ -35,9 +34,12 @@ class _HomeScreenState extends State<HomeScreen> {
Future<void> _loadStats() async { Future<void> _loadStats() async {
final repository = context.read<SessionRepository>(); final repository = context.read<SessionRepository>();
final stats = await repository.getStatistics(); final stats = await repository.getStatistics();
final sessions = await repository.getAllSessions();
if (mounted) { if (mounted) {
setState(() { setState(() {
_stats = stats; _stats = stats;
_recentSessions = sessions;
_isLoading = false; _isLoading = false;
}); });
} }
@@ -47,18 +49,14 @@ class _HomeScreenState extends State<HomeScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
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(
icon: const Icon(Icons.analytics),
onPressed: () => _navigateToStatistics(context),
tooltip: 'Statistiques',
),
IconButton(
icon: const Icon(Icons.history),
onPressed: () => _navigateToHistory(context),
tooltip: 'Historique',
),
IconButton( IconButton(
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
onPressed: () => _navigateToSettings(context), onPressed: () => _navigateToSettings(context),
@@ -74,15 +72,10 @@ class _HomeScreenState extends State<HomeScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// App logo/header
_buildHeader(), _buildHeader(),
const SizedBox(height: AppConstants.largePadding), const SizedBox(height: AppConstants.largePadding),
// Main action button
_buildMainActionButton(context), _buildMainActionButton(context),
const SizedBox(height: AppConstants.largePadding), const SizedBox(height: AppConstants.largePadding),
// Statistics section
if (_isLoading) if (_isLoading)
const Center(child: CircularProgressIndicator()) const Center(child: CircularProgressIndicator())
else if (_stats != null) else if (_stats != null)
@@ -112,16 +105,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,
), ),
], ],
@@ -129,20 +122,23 @@ class _HomeScreenState extends State<HomeScreen> {
} }
Widget _buildMainActionButton(BuildContext context) { Widget _buildMainActionButton(BuildContext context) {
final sessionProvider = context.watch<SessionProvider>();
final isSessionActive = sessionProvider.isSessionActive;
return ElevatedButton.icon( return ElevatedButton.icon(
onPressed: () => _navigateToCapture(context), onPressed: () => _navigateToSessionSetup(context),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor, backgroundColor: isSessionActive ? AppTheme.secondaryColor : AppTheme.primaryColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 20), padding: const EdgeInsets.symmetric(vertical: 20),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConstants.borderRadius), borderRadius: BorderRadius.circular(AppConstants.borderRadius),
), ),
), ),
icon: const Icon(Icons.add_a_photo, size: 28), icon: Icon(isSessionActive ? Icons.play_circle_outline : Icons.add_circle_outline, size: 28),
label: const Text( label: Text(
'Nouvelle Analyse', isSessionActive ? 'Continuer la session' : 'Démarrer la session',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
); );
} }
@@ -153,14 +149,13 @@ 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),
Row( Row(
children: [ children: [
// --- BOUTON SESSIONS (Redirige vers Statistiques) ---
Expanded( Expanded(
child: InkWell( child: InkWell(
onTap: () => _navigateToStatistics(context), onTap: () => _navigateToStatistics(context),
@@ -174,7 +169,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,
@@ -185,10 +179,40 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
], ],
), ),
const SizedBox(height: 12), Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Container(
height: 150,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: _recentSessions.length < 2
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.show_chart, size: 40, color: Colors.grey),
Text(
'Pas assez de données',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
)
: _buildHomeTrendChart(),
),
),
Row( Row(
children: [ children: [
// --- BOUTON SCORE MOYEN (Redirige vers Historique) ---
Expanded( Expanded(
child: InkWell( child: InkWell(
onTap: () => _navigateToHistory(context), onTap: () => _navigateToHistory(context),
@@ -202,7 +226,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,
@@ -217,12 +240,19 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
void _navigateToCapture(BuildContext context) async { void _navigateToSessionSetup(BuildContext context) async {
await Navigator.push( final sessionProvider = context.read<SessionProvider>();
context, if (sessionProvider.isSessionActive) {
MaterialPageRoute(builder: (_) => const CaptureScreen()), await Navigator.push(
); context,
// Refresh stats when returning MaterialPageRoute(builder: (_) => const CaptureScreen()),
);
} else {
await Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SessionSetupScreen()),
);
}
_loadStats(); _loadStats();
} }
@@ -231,7 +261,6 @@ class _HomeScreenState extends State<HomeScreen> {
context, context,
MaterialPageRoute(builder: (_) => const HistoryScreen()), MaterialPageRoute(builder: (_) => const HistoryScreen()),
); );
// Refresh stats when returning
_loadStats(); _loadStats();
} }
@@ -240,14 +269,107 @@ class _HomeScreenState extends State<HomeScreen> {
context, context,
MaterialPageRoute(builder: (_) => const StatisticsScreen()), MaterialPageRoute(builder: (_) => const StatisticsScreen()),
); );
// Refresh stats when returning
_loadStats(); _loadStats();
} }
void _navigateToSettings(BuildContext context) { Widget _buildHomeTrendChart() {
Navigator.push( final sorted = List<Session>.from(_recentSessions)
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
final display =
sorted.length > 7 ? sorted.sublist(sorted.length - 7) : sorted;
return LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (value) => FlLine(
color: Colors.grey.withValues(alpha: 0.1),
strokeWidth: 1,
),
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 22,
interval: 1,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index < 0 || index >= display.length) return const SizedBox();
final date = display[index].createdAt;
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'${date.day}/${date.month}',
style: TextStyle(
fontSize: 9,
color: Colors.grey.withValues(alpha: 0.8),
),
),
);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: TextStyle(
fontSize: 9,
color: Colors.grey.withValues(alpha: 0.8),
),
);
},
),
),
),
borderData: FlBorderData(
show: true,
border: Border(
bottom: BorderSide(color: Colors.grey.withValues(alpha: 0.2)),
left: BorderSide(color: Colors.grey.withValues(alpha: 0.2)),
),
),
lineBarsData: [
LineChartBarData(
spots: display.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), e.value.averageScore);
}).toList(),
isCurved: true,
color: AppTheme.primaryColor,
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter(
radius: 3,
color: Colors.white,
strokeWidth: 2,
strokeColor: AppTheme.primaryColor,
),
),
belowBarData: BarAreaData(
show: true,
color: AppTheme.primaryColor.withValues(alpha: 0.1),
),
),
],
),
);
}
void _navigateToSettings(BuildContext context) async {
await Navigator.push(
context, context,
MaterialPageRoute(builder: (_) => const SettingsScreen()), MaterialPageRoute(builder: (_) => const SettingsScreen()),
); );
_loadStats();
} }
} }

View File

@@ -0,0 +1,51 @@
import 'package:flutter/foundation.dart';
import '../../data/models/session.dart';
import '../../data/models/target_analysis.dart';
import '../../data/models/weapon.dart';
class SessionProvider extends ChangeNotifier {
String? _currentWeaponName;
String? _currentWeaponId;
int _shotsPerTarget = 5;
int _distance = 25;
String? _activeSessionId;
final List<TargetAnalysis> _currentAnalyses = [];
bool _isSessionActive = false;
String? get currentWeapon => _currentWeaponName;
String? get currentWeaponId => _currentWeaponId;
int get shotsPerTarget => _shotsPerTarget;
int get distance => _distance;
bool get isSessionActive => _isSessionActive;
String? get activeSessionId => _activeSessionId;
List<TargetAnalysis> get currentAnalyses => List.unmodifiable(_currentAnalyses);
int get totalSessionScore => _currentAnalyses.fold(0, (sum, a) => sum + a.totalScore);
int get targetCount => _currentAnalyses.length;
void startSession(String weaponName, int shots, String sessionId, {String? weaponId, int distance = 25}) {
_currentWeaponName = weaponName;
_currentWeaponId = weaponId;
_shotsPerTarget = shots;
_distance = distance;
_activeSessionId = sessionId;
_currentAnalyses.clear();
_isSessionActive = true;
notifyListeners();
}
void addAnalysis(TargetAnalysis analysis) {
_currentAnalyses.add(analysis);
notifyListeners();
}
void endSession() {
_isSessionActive = false;
_activeSessionId = null;
_currentWeaponId = null;
_currentWeaponName = null;
_currentAnalyses.clear();
_distance = 25;
notifyListeners();
}
}

View File

@@ -0,0 +1,259 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../core/constants/app_constants.dart';
import '../../core/theme/app_theme.dart';
import '../../data/models/weapon.dart';
import '../../data/repositories/session_repository.dart';
import 'session_provider.dart';
import '../capture/capture_screen.dart';
class SessionSetupScreen extends StatefulWidget {
const SessionSetupScreen({super.key});
@override
State<SessionSetupScreen> createState() => _SessionSetupScreenState();
}
class _SessionSetupScreenState extends State<SessionSetupScreen> {
final _formKey = GlobalKey<FormState>();
final _weaponController = TextEditingController();
int _shotsPerTarget = 5;
int _distance = 25;
List<Weapon> _availableWeapons = [];
Weapon? _selectedWeapon;
bool _isLoadingWeapons = true;
@override
void initState() {
super.initState();
_loadWeapons();
}
Future<void> _loadWeapons() async {
final repository = context.read<SessionRepository>();
final weapons = await repository.getWeapons();
if (mounted) {
setState(() {
_availableWeapons = weapons;
_isLoadingWeapons = false;
});
}
}
@override
void dispose() {
_weaponController.dispose();
super.dispose();
}
void _startSession() {
if (_formKey.currentState!.validate()) {
final repository = context.read<SessionRepository>();
final sessionProvider = context.read<SessionProvider>();
final sessionId = repository.generateId();
sessionProvider.startSession(
_selectedWeapon != null ? _selectedWeapon!.displayName : _weaponController.text,
_shotsPerTarget,
sessionId,
weaponId: _selectedWeapon?.id,
distance: _distance,
);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const CaptureScreen()),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Configuration de la session'),
),
body: _isLoadingWeapons
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Choix de l\'arme',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
if (_availableWeapons.isNotEmpty) ...[
DropdownButtonFormField<Weapon?>(
value: _selectedWeapon,
decoration: InputDecoration(
labelText: 'Sélectionner une arme de l\'armurerie',
prefixIcon: const Icon(Icons.shield),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
),
),
items: [
const DropdownMenuItem<Weapon?>(
value: null,
child: Text('Nouvelle arme / Autre'),
),
..._availableWeapons.map((w) => DropdownMenuItem(
value: w,
child: Text(w.displayName),
)),
],
onChanged: (value) {
setState(() {
_selectedWeapon = value;
if (value != null) {
_weaponController.text = value.displayName;
_shotsPerTarget = value.magazineCapacity;
// Auto-set distance based on weapon type if needed
_distance = (value.type == WeaponType.handgun) ? 25 : 50;
} else {
_weaponController.clear();
}
});
},
),
const SizedBox(height: 16),
const Center(child: Text('OU', style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold))),
const SizedBox(height: 16),
],
// Weapon field (manual entry if no weapon selected)
TextFormField(
controller: _weaponController,
enabled: _selectedWeapon == null,
decoration: InputDecoration(
labelText: 'Nom de l\'arme',
hintText: 'ex: Glock 17, CZ Shadow 2...',
prefixIcon: const Icon(Icons.edit),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez renseigner votre arme';
}
return null;
},
),
const SizedBox(height: 24),
// Distance selector
const Text(
'Distance de tir (mètres)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Slider(
value: _distance.toDouble(),
min: 5,
max: 300,
divisions: 59, // (300-5)/5 = 59 divisions for 5m steps
label: '${_distance}m',
onChanged: (value) {
setState(() {
_distance = value.round();
});
},
),
),
Container(
width: 70,
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppTheme.secondaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${_distance}m',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.secondaryColor,
),
),
),
],
),
const SizedBox(height: 24),
// Shots per target
const Text(
'Nombre de balles par cible',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Slider(
value: _shotsPerTarget.toDouble(),
min: 1,
max: 30,
divisions: 29,
label: '$_shotsPerTarget',
onChanged: (value) {
setState(() {
_shotsPerTarget = value.round();
});
},
),
),
Container(
width: 50,
height: 50,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$_shotsPerTarget',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
),
],
),
const SizedBox(height: 48),
ElevatedButton.icon(
onPressed: _startSession,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
),
),
icon: const Icon(Icons.play_arrow),
label: const Text(
'DÉMARRER LA SESSION',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
],
),
),
),
);
}
}

View File

@@ -1,11 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../core/theme/theme_provider.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
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 '../../services/wallet_identity_service.dart'; import '../../services/wallet_identity_service.dart';
import '../garage/weapon_list_screen.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@@ -81,6 +84,49 @@ class _SettingsScreenState extends State<SettingsScreen> {
} }
} }
void _showThemeDialog() {
final themeProvider = context.read<ThemeProvider>();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Apparence'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildThemeOption(ThemeMode.system, 'Automatique', Icons.brightness_auto),
_buildThemeOption(ThemeMode.light, 'Clair', Icons.light_mode),
_buildThemeOption(ThemeMode.dark, 'Sombre', Icons.dark_mode),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Fermer'),
),
],
),
);
}
Widget _buildThemeOption(ThemeMode mode, String label, IconData icon) {
final themeProvider = context.watch<ThemeProvider>();
final isSelected = themeProvider.themeMode == mode;
return ListTile(
leading: Icon(icon, color: isSelected ? AppTheme.primaryColor : null),
title: Text(label, style: TextStyle(
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? AppTheme.primaryColor : null,
)),
trailing: isSelected ? const Icon(Icons.check, color: AppTheme.primaryColor) : null,
onTap: () {
themeProvider.setThemeMode(mode);
Navigator.pop(context);
},
);
}
void _showIdentityDialog() { void _showIdentityDialog() {
if (_identityPhrase == null) return; if (_identityPhrase == null) return;
@@ -218,14 +264,29 @@ class _SettingsScreenState extends State<SettingsScreen> {
subtitle: _identityPhrase != null ? 'Phrase de 15 mots générée' : 'Génération en cours...', subtitle: _identityPhrase != null ? 'Phrase de 15 mots générée' : 'Génération en cours...',
onTap: _showIdentityDialog, onTap: _showIdentityDialog,
), ),
Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return _buildSettingsTile(
context: context,
icon: Icons.color_lens_outlined,
title: 'Apparence',
subtitle: themeProvider.themeModeName,
onTap: _showThemeDialog,
);
},
),
const SizedBox(height: 24),
_buildSectionHeader('Gestion'),
_buildSettingsTile( _buildSettingsTile(
context: context, context: context,
icon: Icons.color_lens_outlined, icon: Icons.shield_outlined,
title: 'Apparence', title: 'Mon Armurerie',
subtitle: 'Thème clair/sombre (à venir)', subtitle: 'Gérer mes armes et équipements',
onTap: () { onTap: () async {
ScaffoldMessenger.of(context).showSnackBar( await Navigator.push(
const SnackBar(content: Text('Fonctionnalité en développement')), context,
MaterialPageRoute(builder: (_) => const WeaponListScreen()),
); );
}, },
), ),

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,13 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'app.dart'; import 'app.dart';
import 'core/theme/theme_provider.dart';
import 'data/repositories/session_repository.dart'; import 'data/repositories/session_repository.dart';
import 'services/target_detection_service.dart'; 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 'features/session/session_provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -45,6 +47,8 @@ void main() async {
create: (_) => GroupingAnalyzerService(), create: (_) => GroupingAnalyzerService(),
), ),
Provider<SessionRepository>(create: (_) => SessionRepository()), Provider<SessionRepository>(create: (_) => SessionRepository()),
ChangeNotifierProvider<ThemeProvider>(create: (_) => ThemeProvider()),
ChangeNotifierProvider<SessionProvider>(create: (_) => SessionProvider()),
], ],
child: const BullyApp(), child: const BullyApp(),
), ),

View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'features/home/home_screen.dart';
import 'features/history/history_screen.dart';
import 'features/statistics/statistics_screen.dart';
import 'features/garage/weapon_list_screen.dart';
import 'core/theme/app_theme.dart';
class MainNavigationHolder extends StatefulWidget {
const MainNavigationHolder({super.key});
@override
State<MainNavigationHolder> createState() => _MainNavigationHolderState();
}
class _MainNavigationHolderState extends State<MainNavigationHolder> {
int _selectedIndex = 0;
final List<Widget> _screens = [
const HomeScreen(),
const HistoryScreen(),
const StatisticsScreen(),
const WeaponListScreen(),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _screens,
),
bottomNavigationBar: Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: _onItemTapped,
type: BottomNavigationBarType.fixed,
backgroundColor: Theme.of(context).cardColor,
selectedItemColor: AppTheme.primaryColor,
unselectedItemColor: Colors.grey,
showUnselectedLabels: true,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: 'Accueil',
),
BottomNavigationBarItem(
icon: Icon(Icons.history_outlined),
activeIcon: Icon(Icons.history),
label: 'Historique',
),
BottomNavigationBarItem(
icon: Icon(Icons.analytics_outlined),
activeIcon: Icon(Icons.analytics),
label: 'Stats',
),
BottomNavigationBarItem(
icon: Icon(Icons.shield_outlined),
activeIcon: Icon(Icons.shield),
label: 'Armurerie',
),
],
),
),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'dart:io'; import 'dart:io';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
@@ -37,7 +38,7 @@ class AiExportService {
deviceData['os'] = 'Windows ${windowsInfo.majorVersion}.${windowsInfo.minorVersion}'; deviceData['os'] = 'Windows ${windowsInfo.majorVersion}.${windowsInfo.minorVersion}';
} }
} catch (e) { } catch (e) {
print('Erreur lors de la récupération des infos appareil: $e'); debugPrint('Erreur lors de la récupération des infos appareil: $e');
} }
return deviceData; return deviceData;
@@ -131,15 +132,15 @@ class AiExportService {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final responseData = await response.stream.bytesToString(); final responseData = await response.stream.bytesToString();
print('Export réussi: $responseData'); debugPrint('Export réussi: $responseData');
return true; return true;
} else { } else {
final errorData = await response.stream.bytesToString(); final errorData = await response.stream.bytesToString();
print('Erreur d\'export: ${response.statusCode} - $errorData'); debugPrint('Erreur d\'export: ${response.statusCode} - $errorData');
return false; return false;
} }
} catch (e) { } catch (e) {
print('Exception lors de l\'export: $e'); debugPrint('Exception lors de l\'export: $e');
return false; return false;
} }
} }

View File

@@ -8,6 +8,7 @@ library;
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
import 'package:flutter/foundation.dart';
import 'package:opencv_dart/opencv_dart.dart' as cv; import 'package:opencv_dart/opencv_dart.dart' as cv;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -562,8 +563,9 @@ class DistortionCorrectionService {
double maxArea = 0; double maxArea = 0;
for (final contour in contours) { for (final contour in contours) {
if (contour.length < 5) if (contour.length < 5) {
continue; // fitEllipse nécessite au moins 5 points continue; // fitEllipse nécessite au moins 5 points
}
final area = cv.contourArea(contour); final area = cv.contourArea(contour);
if (area < 1000) continue; // Ignorer les trop petits bruits if (area < 1000) continue; // Ignorer les trop petits bruits
@@ -636,7 +638,7 @@ class DistortionCorrectionService {
return outputPath; return outputPath;
} catch (e) { } catch (e) {
// En cas d'erreur, retourner l'image originale // En cas d'erreur, retourner l'image originale
print('Erreur correction perspective cercles: $e'); debugPrint('Erreur correction perspective cercles: $e');
return imagePath; return imagePath;
} }
} }
@@ -761,7 +763,7 @@ class DistortionCorrectionService {
return outputPath; return outputPath;
} catch (e) { } catch (e) {
print('Erreur correction perspective ovales: $e'); debugPrint('Erreur correction perspective ovales: $e');
return imagePath; return imagePath;
} }
} }
@@ -822,7 +824,7 @@ class DistortionCorrectionService {
} }
if (concentricGroup.length < 2) { if (concentricGroup.length < 2) {
print( debugPrint(
"Pas assez de cercles concentriques pour le maillage, utilisation de la méthode simple.", "Pas assez de cercles concentriques pour le maillage, utilisation de la méthode simple.",
); );
return await correctPerspectiveUsingOvals(imagePath); return await correctPerspectiveUsingOvals(imagePath);
@@ -950,7 +952,7 @@ class DistortionCorrectionService {
return outputPath; return outputPath;
} catch (e) { } catch (e) {
print('Erreur correction perspective maillage concentrique: $e'); debugPrint('Erreur correction perspective maillage concentrique: $e');
return imagePath; return imagePath;
} }
} }
@@ -1012,7 +1014,7 @@ class DistortionCorrectionService {
// Fallback // Fallback
if (bestQuad == null) { if (bestQuad == null) {
print( debugPrint(
"Aucun papier quadrilatère détecté, on utilise les cercles à la place.", "Aucun papier quadrilatère détecté, on utilise les cercles à la place.",
); );
return await correctPerspectiveUsingCircles(imagePath); return await correctPerspectiveUsingCircles(imagePath);
@@ -1060,7 +1062,7 @@ class DistortionCorrectionService {
return outputPath; return outputPath;
} catch (e) { } catch (e) {
print('Erreur correction perspective quadrilatère: $e'); debugPrint('Erreur correction perspective quadrilatère: $e');
// Fallback // Fallback
return await correctPerspectiveUsingCircles(imagePath); return await correctPerspectiveUsingCircles(imagePath);
} }

View File

@@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:image/image.dart' as img; import 'package:image/image.dart' as img;
@@ -189,7 +190,7 @@ class ImageProcessingService {
); );
}).toList(); }).toList();
} catch (e) { } catch (e) {
print('Error detecting impacts: $e'); debugPrint('Error detecting impacts: $e');
return []; return [];
} }
} }
@@ -236,14 +237,14 @@ class ImageProcessingService {
final fillRatios = <double>[]; final fillRatios = <double>[];
final thresholds = <double>[]; final thresholds = <double>[];
print('Analyzing ${references.length} reference impacts...'); debugPrint('Analyzing ${references.length} reference impacts...');
for (int refIndex = 0; refIndex < references.length; refIndex++) { for (int refIndex = 0; refIndex < references.length; refIndex++) {
final ref = references[refIndex]; final ref = references[refIndex];
final centerX = (ref.x * width).round().clamp(0, width - 1); final centerX = (ref.x * width).round().clamp(0, width - 1);
final centerY = (ref.y * height).round().clamp(0, height - 1); final centerY = (ref.y * height).round().clamp(0, height - 1);
print('Reference $refIndex at ($centerX, $centerY)'); debugPrint('Reference $refIndex at ($centerX, $centerY)');
// AMÉLIORATION : Recherche du point le plus sombre dans une zone plus large // AMÉLIORATION : Recherche du point le plus sombre dans une zone plus large
int darkestX = centerX; int darkestX = centerX;
@@ -275,7 +276,7 @@ class ImageProcessingService {
if (darkestLum < 50 && r > 5) break; if (darkestLum < 50 && r > 5) break;
} }
print(' Darkest point at ($darkestX, $darkestY), lum=$darkestLum'); debugPrint(' Darkest point at ($darkestX, $darkestY), lum=$darkestLum');
// Now find the blob at the darkest point using adaptive threshold // Now find the blob at the darkest point using adaptive threshold
final blobResult = _findBlobAtPoint(blurred, darkestX, darkestY, width, height); final blobResult = _findBlobAtPoint(blurred, darkestX, darkestY, width, height);
@@ -286,15 +287,15 @@ class ImageProcessingService {
circularities.add(blobResult.circularity); circularities.add(blobResult.circularity);
fillRatios.add(blobResult.fillRatio); fillRatios.add(blobResult.fillRatio);
thresholds.add(blobResult.threshold); thresholds.add(blobResult.threshold);
print(' Found blob: size=${blobResult.size}, circ=${blobResult.circularity.toStringAsFixed(2)}, ' debugPrint(' Found blob: size=${blobResult.size}, circ=${blobResult.circularity.toStringAsFixed(2)}, '
'fill=${blobResult.fillRatio.toStringAsFixed(2)}, threshold=${blobResult.threshold.toStringAsFixed(0)}'); 'fill=${blobResult.fillRatio.toStringAsFixed(2)}, threshold=${blobResult.threshold.toStringAsFixed(0)}');
} else { } else {
print(' No valid blob found at this reference'); debugPrint(' No valid blob found at this reference');
} }
} }
if (luminances.isEmpty) { if (luminances.isEmpty) {
print('ERROR: No valid blobs found from any reference!'); debugPrint('ERROR: No valid blobs found from any reference!');
return null; return null;
} }
@@ -329,11 +330,11 @@ class ImageProcessingService {
avgDarkThreshold: avgThreshold, avgDarkThreshold: avgThreshold,
); );
print('Learned characteristics: $result'); debugPrint('Learned characteristics: $result');
return result; return result;
} catch (e) { } catch (e) {
print('Error analyzing reference impacts: $e'); debugPrint('Error analyzing reference impacts: $e');
return null; return null;
} }
} }
@@ -680,7 +681,7 @@ class ImageProcessingService {
// Calculate minimum fill ratio - impacts pleins // Calculate minimum fill ratio - impacts pleins
final minFillRatio = (characteristics.avgFillRatio - 0.2).clamp(0.35, 0.85); final minFillRatio = (characteristics.avgFillRatio - 0.2).clamp(0.35, 0.85);
print('Detection params: thresholds=$thresholds, size=$minSize-$maxSize, ' debugPrint('Detection params: thresholds=$thresholds, size=$minSize-$maxSize, '
'circ>=$effectiveMinCircularity, fill>=$minFillRatio'); 'circ>=$effectiveMinCircularity, fill>=$minFillRatio');
// Détecter avec plusieurs seuils et combiner les résultats // Détecter avec plusieurs seuils et combiner les résultats
@@ -722,7 +723,7 @@ class ImageProcessingService {
return true; return true;
}).toList(); }).toList();
print('Found ${filteredBlobs.length} impacts after filtering (from ${mergedBlobs.length} merged)'); debugPrint('Found ${filteredBlobs.length} impacts after filtering (from ${mergedBlobs.length} merged)');
// Convert to relative coordinates // Convert to relative coordinates
return filteredBlobs.map((blob) { return filteredBlobs.map((blob) {
@@ -733,7 +734,7 @@ class ImageProcessingService {
); );
}).toList(); }).toList();
} catch (e) { } catch (e) {
print('Error detecting impacts from references: $e'); debugPrint('Error detecting impacts from references: $e');
return []; return [];
} }
} }
@@ -776,50 +777,6 @@ class ImageProcessingService {
return merged; return merged;
} }
/// Detect dark spots with adaptive luminance range
List<_Blob> _detectDarkSpotsAdaptive(
img.Image image,
int minLuminance,
int maxLuminance,
int minSize,
int maxSize, {
double minCircularity = 0.5,
double minFillRatio = 0.5,
}) {
final width = image.width;
final height = image.height;
// Create binary mask of pixels within luminance range
final mask = List.generate(height, (_) => List.filled(width, false));
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
final pixel = image.getPixel(x, y);
final luminance = img.getLuminance(pixel);
mask[y][x] = luminance >= minLuminance && luminance <= maxLuminance;
}
}
// Find connected components
final visited = List.generate(height, (_) => List.filled(width, false));
final blobs = <_Blob>[];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (mask[y][x] && !visited[y][x]) {
final blob = _floodFill(mask, visited, x, y, width, height);
if (blob.size >= minSize &&
blob.size <= maxSize &&
blob.circularity >= minCircularity &&
blob.fillRatio >= minFillRatio) {
blobs.add(blob);
}
}
}
}
return _filterOverlappingBlobs(blobs);
}
/// Detect dark spots in a grayscale image using blob detection /// Detect dark spots in a grayscale image using blob detection
List<_Blob> _detectDarkSpots( List<_Blob> _detectDarkSpots(

View File

@@ -66,25 +66,12 @@ class PrecisionStats {
/// Standard deviation statistics /// Standard deviation statistics
class StdDevStats { class StdDevStats {
/// Standard deviation of X positions
final double stdDevX; final double stdDevX;
/// Standard deviation of Y positions
final double stdDevY; final double stdDevY;
/// Combined standard deviation (radial)
final double stdDevRadial; final double stdDevRadial;
/// Standard deviation of scores
final double stdDevScore; final double stdDevScore;
/// Mean X position
final double meanX; final double meanX;
/// Mean Y position
final double meanY; final double meanY;
/// Mean score
final double meanScore; final double meanScore;
const StdDevStats({ const StdDevStats({
@@ -98,18 +85,11 @@ class StdDevStats {
}); });
} }
/// Regional distribution (quadrants or sectors) /// Regional distribution
class RegionalStats { class RegionalStats {
/// Shot distribution by quadrant (top-left, top-right, bottom-left, bottom-right)
final Map<String, int> quadrantDistribution; final Map<String, int> quadrantDistribution;
/// Shot distribution by sector (N, NE, E, SE, S, SW, W, NW, Center)
final Map<String, int> sectorDistribution; final Map<String, int> sectorDistribution;
/// Dominant direction (where most shots land)
final String dominantDirection; final String dominantDirection;
/// Bias offset from center
final double biasX; final double biasX;
final double biasY; final double biasY;
@@ -157,16 +137,16 @@ class StatisticsService {
SessionStatistics calculateStatistics( SessionStatistics calculateStatistics(
List<Session> sessions, { List<Session> sessions, {
StatsPeriod period = StatsPeriod.all, StatsPeriod period = StatsPeriod.all,
double targetCenterX = 0.5,
double targetCenterY = 0.5,
}) { }) {
// Filter sessions by period // Filter sessions by period
final filteredSessions = _filterByPeriod(sessions, period); final filteredSessions = _filterByPeriod(sessions, period);
// Collect all shots // Collect all shots from all analyses in all sessions
final allShots = <Shot>[]; final allShots = <Shot>[];
for (final session in filteredSessions) { for (final session in filteredSessions) {
allShots.addAll(session.shots); for (final analysis in session.analyses) {
allShots.addAll(analysis.shots);
}
} }
if (allShots.isEmpty) { if (allShots.isEmpty) {
@@ -183,14 +163,14 @@ class StatisticsService {
// Calculate heat map // Calculate heat map
final heatMap = _calculateHeatMap(allShots, gridSize: 5); final heatMap = _calculateHeatMap(allShots, gridSize: 5);
// Calculate precision // Calculate precision (using relative center 0.5, 0.5)
final precision = _calculatePrecision(allShots, targetCenterX, targetCenterY); final precision = _calculatePrecision(allShots, 0.5, 0.5);
// Calculate standard deviation // Calculate standard deviation
final stdDev = _calculateStdDev(allShots); final stdDev = _calculateStdDev(allShots);
// Calculate regional distribution // Calculate regional distribution
final regional = _calculateRegional(allShots, targetCenterX, targetCenterY); final regional = _calculateRegional(allShots, 0.5, 0.5);
return SessionStatistics( return SessionStatistics(
totalShots: totalShots, totalShots: totalShots,
@@ -224,20 +204,13 @@ class StatisticsService {
/// Calculate heat map /// Calculate heat map
HeatMap _calculateHeatMap(List<Shot> shots, {int gridSize = 5}) { HeatMap _calculateHeatMap(List<Shot> shots, {int gridSize = 5}) {
// Initialize grid final grid = List.generate(gridSize, (_) => List.generate(gridSize, (_) => <Shot>[]));
final grid = List.generate(
gridSize,
(_) => List.generate(gridSize, (_) => <Shot>[]),
);
// Assign shots to grid cells
for (final shot in shots) { for (final shot in shots) {
final col = (shot.x * gridSize).floor().clamp(0, gridSize - 1); final col = (shot.x * gridSize).floor().clamp(0, gridSize - 1);
final row = (shot.y * gridSize).floor().clamp(0, gridSize - 1); final row = (shot.y * gridSize).floor().clamp(0, gridSize - 1);
grid[row][col].add(shot); grid[row][col].add(shot);
} }
// Find max count for normalization
int maxCount = 0; int maxCount = 0;
for (final row in grid) { for (final row in grid) {
for (final cell in row) { for (final cell in row) {
@@ -245,16 +218,12 @@ class StatisticsService {
} }
} }
// Create heat zones
final zones = <List<HeatZone>>[]; final zones = <List<HeatZone>>[];
for (int row = 0; row < gridSize; row++) { for (int row = 0; row < gridSize; row++) {
final rowZones = <HeatZone>[]; final rowZones = <HeatZone>[];
for (int col = 0; col < gridSize; col++) { for (int col = 0; col < gridSize; col++) {
final cellShots = grid[row][col]; final cellShots = grid[row][col];
final avgScore = cellShots.isEmpty final avgScore = cellShots.isEmpty ? 0.0 : cellShots.fold<int>(0, (sum, s) => sum + s.score) / cellShots.length;
? 0.0
: cellShots.fold<int>(0, (sum, s) => sum + s.score) / cellShots.length;
rowZones.add(HeatZone( rowZones.add(HeatZone(
row: row, row: row,
col: col, col: col,
@@ -266,30 +235,15 @@ class StatisticsService {
zones.add(rowZones); zones.add(rowZones);
} }
return HeatMap( return HeatMap(gridSize: gridSize, zones: zones, maxShotsInZone: maxCount, totalShots: shots.length);
gridSize: gridSize,
zones: zones,
maxShotsInZone: maxCount,
totalShots: shots.length,
);
} }
/// Calculate precision statistics /// Calculate precision statistics
PrecisionStats _calculatePrecision( PrecisionStats _calculatePrecision(List<Shot> shots, double centerX, double centerY) {
List<Shot> shots,
double centerX,
double centerY,
) {
if (shots.isEmpty) { if (shots.isEmpty) {
return const PrecisionStats( return const PrecisionStats(avgDistanceFromCenter: 0, groupingDiameter: 0, precisionScore: 0, consistencyScore: 0);
avgDistanceFromCenter: 0,
groupingDiameter: 0,
precisionScore: 0,
consistencyScore: 0,
);
} }
// Calculate distances from center
final distances = shots.map((shot) { final distances = shots.map((shot) {
final dx = shot.x - centerX; final dx = shot.x - centerX;
final dy = shot.y - centerY; final dy = shot.y - centerY;
@@ -297,8 +251,6 @@ class StatisticsService {
}).toList(); }).toList();
final avgDistance = distances.reduce((a, b) => a + b) / distances.length; final avgDistance = distances.reduce((a, b) => a + b) / distances.length;
// Calculate grouping (spread between shots)
double maxSpread = 0; double maxSpread = 0;
for (int i = 0; i < shots.length; i++) { for (int i = 0; i < shots.length; i++) {
for (int j = i + 1; j < shots.length; j++) { for (int j = i + 1; j < shots.length; j++) {
@@ -309,45 +261,29 @@ class StatisticsService {
} }
} }
// Calculate standard deviation of distances (consistency)
final meanDist = avgDistance;
double variance = 0; double variance = 0;
for (final d in distances) { for (final d in distances) {
variance += math.pow(d - meanDist, 2); variance += math.pow(d - avgDistance, 2);
} }
final stdDevDist = math.sqrt(variance / distances.length); final stdDevDist = math.sqrt(variance / distances.length);
// Precision score: based on average distance from center (0-100)
// 0 distance = 100 score, 0.5 distance = 0 score
final precisionScore = math.max(0, (1 - avgDistance * 2) * 100); final precisionScore = math.max(0, (1 - avgDistance * 2) * 100);
// Consistency score: based on grouping tightness (0-100)
// Lower spread = higher consistency
final consistencyScore = math.max(0, (1 - stdDevDist * 5) * 100); final consistencyScore = math.max(0, (1 - stdDevDist * 5) * 100);
return PrecisionStats( return PrecisionStats(
avgDistanceFromCenter: avgDistance.toDouble(), avgDistanceFromCenter: avgDistance,
groupingDiameter: maxSpread.toDouble(), groupingDiameter: maxSpread,
precisionScore: precisionScore.clamp(0.0, 100.0).toDouble(), precisionScore: precisionScore.toDouble().clamp(0.0, 100.0),
consistencyScore: consistencyScore.clamp(0.0, 100.0).toDouble(), consistencyScore: consistencyScore.toDouble().clamp(0.0, 100.0),
); );
} }
/// Calculate standard deviation statistics /// Calculate standard deviation statistics
StdDevStats _calculateStdDev(List<Shot> shots) { StdDevStats _calculateStdDev(List<Shot> shots) {
if (shots.isEmpty) { if (shots.isEmpty) {
return const StdDevStats( return const StdDevStats(stdDevX: 0, stdDevY: 0, stdDevRadial: 0, stdDevScore: 0, meanX: 0.5, meanY: 0.5, meanScore: 0);
stdDevX: 0,
stdDevY: 0,
stdDevRadial: 0,
stdDevScore: 0,
meanX: 0.5,
meanY: 0.5,
meanScore: 0,
);
} }
// Calculate means
double sumX = 0, sumY = 0, sumScore = 0; double sumX = 0, sumY = 0, sumScore = 0;
for (final shot in shots) { for (final shot in shots) {
sumX += shot.x; sumX += shot.x;
@@ -358,7 +294,6 @@ class StatisticsService {
final meanY = sumY / shots.length; final meanY = sumY / shots.length;
final meanScore = sumScore / shots.length; final meanScore = sumScore / shots.length;
// Calculate variances
double varianceX = 0, varianceY = 0, varianceScore = 0; double varianceX = 0, varianceY = 0, varianceScore = 0;
for (final shot in shots) { for (final shot in shots) {
varianceX += math.pow(shot.x - meanX, 2); varianceX += math.pow(shot.x - meanX, 2);
@@ -369,18 +304,11 @@ class StatisticsService {
varianceY /= shots.length; varianceY /= shots.length;
varianceScore /= shots.length; varianceScore /= shots.length;
final stdDevX = math.sqrt(varianceX);
final stdDevY = math.sqrt(varianceY);
final stdDevScore = math.sqrt(varianceScore);
// Radial standard deviation
final stdDevRadial = math.sqrt(varianceX + varianceY);
return StdDevStats( return StdDevStats(
stdDevX: stdDevX, stdDevX: math.sqrt(varianceX),
stdDevY: stdDevY, stdDevY: math.sqrt(varianceY),
stdDevRadial: stdDevRadial, stdDevRadial: math.sqrt(varianceX + varianceY),
stdDevScore: stdDevScore, stdDevScore: math.sqrt(varianceScore),
meanX: meanX, meanX: meanX,
meanY: meanY, meanY: meanY,
meanScore: meanScore, meanScore: meanScore,
@@ -388,42 +316,13 @@ class StatisticsService {
} }
/// Calculate regional distribution /// Calculate regional distribution
RegionalStats _calculateRegional( RegionalStats _calculateRegional(List<Shot> shots, double centerX, double centerY) {
List<Shot> shots,
double centerX,
double centerY,
) {
if (shots.isEmpty) { if (shots.isEmpty) {
return const RegionalStats( return const RegionalStats(quadrantDistribution: {}, sectorDistribution: {}, dominantDirection: 'Centre', biasX: 0, biasY: 0);
quadrantDistribution: {},
sectorDistribution: {},
dominantDirection: 'Centre',
biasX: 0,
biasY: 0,
);
} }
// Quadrant distribution final quadrants = <String, int>{'Haut-Gauche': 0, 'Haut-Droite': 0, 'Bas-Gauche': 0, 'Bas-Droite': 0};
final quadrants = <String, int>{ final sectors = <String, int>{'N': 0, 'NE': 0, 'E': 0, 'SE': 0, 'S': 0, 'SO': 0, 'O': 0, 'NO': 0, 'Centre': 0};
'Haut-Gauche': 0,
'Haut-Droite': 0,
'Bas-Gauche': 0,
'Bas-Droite': 0,
};
// Sector distribution (8 sectors + center)
final sectors = <String, int>{
'N': 0,
'NE': 0,
'E': 0,
'SE': 0,
'S': 0,
'SO': 0,
'O': 0,
'NO': 0,
'Centre': 0,
};
double sumDx = 0, sumDy = 0; double sumDx = 0, sumDy = 0;
for (final shot in shots) { for (final shot in shots) {
@@ -432,16 +331,12 @@ class StatisticsService {
sumDx += dx; sumDx += dx;
sumDy += dy; sumDy += dy;
// Quadrant
if (dy < 0) { if (dy < 0) {
quadrants[dx < 0 ? 'Haut-Gauche' : 'Haut-Droite'] = quadrants[dx < 0 ? 'Haut-Gauche' : 'Haut-Droite'] = quadrants[dx < 0 ? 'Haut-Gauche' : 'Haut-Droite']! + 1;
quadrants[dx < 0 ? 'Haut-Gauche' : 'Haut-Droite']! + 1;
} else { } else {
quadrants[dx < 0 ? 'Bas-Gauche' : 'Bas-Droite'] = quadrants[dx < 0 ? 'Bas-Gauche' : 'Bas-Droite'] = quadrants[dx < 0 ? 'Bas-Gauche' : 'Bas-Droite']! + 1;
quadrants[dx < 0 ? 'Bas-Gauche' : 'Bas-Droite']! + 1;
} }
// Sector
final distance = math.sqrt(dx * dx + dy * dy); final distance = math.sqrt(dx * dx + dy * dy);
if (distance < 0.1) { if (distance < 0.1) {
sectors['Centre'] = sectors['Centre']! + 1; sectors['Centre'] = sectors['Centre']! + 1;
@@ -452,11 +347,6 @@ class StatisticsService {
} }
} }
// Calculate bias
final biasX = sumDx / shots.length;
final biasY = sumDy / shots.length;
// Find dominant direction
String dominant = 'Centre'; String dominant = 'Centre';
int maxCount = 0; int maxCount = 0;
sectors.forEach((key, value) { sectors.forEach((key, value) {
@@ -470,14 +360,12 @@ class StatisticsService {
quadrantDistribution: quadrants, quadrantDistribution: quadrants,
sectorDistribution: sectors, sectorDistribution: sectors,
dominantDirection: dominant, dominantDirection: dominant,
biasX: biasX, biasX: sumDx / shots.length,
biasY: biasY, biasY: sumDy / shots.length,
); );
} }
String _angleToSector(double angle) { String _angleToSector(double angle) {
// Angle is in degrees, -180 to 180
// 0 = East, 90 = South, -90 = North, 180/-180 = West
if (angle >= -22.5 && angle < 22.5) return 'E'; if (angle >= -22.5 && angle < 22.5) return 'E';
if (angle >= 22.5 && angle < 67.5) return 'SE'; if (angle >= 22.5 && angle < 67.5) return 'SE';
if (angle >= 67.5 && angle < 112.5) return 'S'; if (angle >= 67.5 && angle < 112.5) return 'S';
@@ -491,41 +379,12 @@ class StatisticsService {
SessionStatistics _emptyStatistics(StatsPeriod period, List<Session> sessions) { SessionStatistics _emptyStatistics(StatsPeriod period, List<Session> sessions) {
return SessionStatistics( return SessionStatistics(
totalShots: 0, totalShots: 0, totalScore: 0, avgScore: 0, maxScore: 0, minScore: 0,
totalScore: 0, heatMap: const HeatMap(gridSize: 5, zones: [], maxShotsInZone: 0, totalShots: 0),
avgScore: 0, precision: const PrecisionStats(avgDistanceFromCenter: 0, groupingDiameter: 0, precisionScore: 0, consistencyScore: 0),
maxScore: 0, stdDev: const StdDevStats(stdDevX: 0, stdDevY: 0, stdDevRadial: 0, stdDevScore: 0, meanX: 0.5, meanY: 0.5, meanScore: 0),
minScore: 0, regional: const RegionalStats(quadrantDistribution: {}, sectorDistribution: {}, dominantDirection: 'Centre', biasX: 0, biasY: 0),
heatMap: const HeatMap( sessions: sessions, period: period,
gridSize: 5,
zones: [],
maxShotsInZone: 0,
totalShots: 0,
),
precision: const PrecisionStats(
avgDistanceFromCenter: 0,
groupingDiameter: 0,
precisionScore: 0,
consistencyScore: 0,
),
stdDev: const StdDevStats(
stdDevX: 0,
stdDevY: 0,
stdDevRadial: 0,
stdDevScore: 0,
meanX: 0.5,
meanY: 0.5,
meanScore: 0,
),
regional: const RegionalStats(
quadrantDistribution: {},
sectorDistribution: {},
dominantDirection: 'Centre',
biasX: 0,
biasY: 0,
),
sessions: sessions,
period: period,
); );
} }
} }

View File

@@ -1,4 +1,5 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart';
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';
@@ -320,7 +321,7 @@ class TargetDetectionService {
); );
}).toList(); }).toList();
} catch (e) { } catch (e) {
print('Erreur détection OpenCV: $e'); debugPrint('Erreur détection OpenCV: $e');
return []; return [];
} }
} }
@@ -369,7 +370,7 @@ class TargetDetectionService {
); );
}).toList(); }).toList();
} catch (e) { } catch (e) {
print('Erreur détection OpenCV depuis références: $e'); debugPrint('Erreur détection OpenCV depuis références: $e');
return []; return [];
} }
} }

View File

@@ -4,6 +4,7 @@ import 'package:crypto/crypto.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
class WalletIdentityService { class WalletIdentityService {
static const String _prefsKey = 'wallet_identity_phrase'; static const String _prefsKey = 'wallet_identity_phrase';
@@ -86,7 +87,7 @@ class WalletIdentityService {
deviceId = windowsInfo.deviceId; deviceId = windowsInfo.deviceId;
} }
} catch (e) { } catch (e) {
print('Erreur lecture device ID: $e'); debugPrint('Erreur lecture device ID: $e');
} }
// Add a salt to the device ID // Add a salt to the device ID

View File

@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

View File

@@ -7,14 +7,12 @@ import Foundation
import device_info_plus import device_info_plus
import file_selector_macos import file_selector_macos
import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) 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"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
} }

View File

@@ -5,18 +5,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.7" version: "4.0.9"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -85,18 +93,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.9"
dartcv4: dartcv4:
dependency: transitive dependency: transitive
description: description:
name: dartcv4 name: dartcv4
sha256: "43dba49162662f3b6e3daf5a95d071429365e2f1ada67d412b851fc9be442e58" sha256: "5764d13550055da3bb35ca28e6866b837579dfb4b594582b7a7753785ca5dfef"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1+1" version: "2.2.1+4"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -197,10 +205,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
@@ -214,14 +222,19 @@ 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:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.33" version: "2.0.34"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -252,10 +265,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: hooks name: hooks
sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.3"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -276,26 +289,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image name: image
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.7.2" version: "4.8.0"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
name: image_picker name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
image_picker_android: image_picker_android:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: "518a16108529fc18657a3e6dde4a043dc465d16596d20ab2abd49a4cac2e703d" sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.13+13" version: "0.8.13+17"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@@ -348,10 +361,26 @@ 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"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -380,10 +409,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.1.0"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@@ -428,18 +457,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: native_toolchain_c name: native_toolchain_c
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.17.4" version: "0.17.6"
native_toolchain_cmake: native_toolchain_cmake:
dependency: transitive dependency: transitive
description: description:
name: native_toolchain_cmake name: native_toolchain_cmake
sha256: fe40e8483183ced98e851e08a9cd2a547fd412cccab98277aa23f2377e43d66f sha256: cd4865568c57f03c7ab1f138cc5032c5f77825d27083d6f5a3e9bbb3e8014a87
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.4" version: "0.2.5"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@@ -448,14 +477,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
opencv_dart: opencv_dart:
dependency: "direct main" dependency: "direct main"
description: description:
name: opencv_dart name: opencv_dart
sha256: c2b7cc614cad69c2857e9b684e3066af662a03fe7100f4dc9a630e81ad42103a sha256: b50ea4788e6d61df6369874e0244445d98ef9210dbb90b8d3c1df198b4ee2333
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1+1" version: "2.2.1+4"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -476,18 +521,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.22" version: "2.3.1"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.1" version: "2.6.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -512,14 +557,62 @@ 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:
name: petitparser name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -540,10 +633,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: posix name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.3" version: "6.5.0"
provider: provider:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -560,6 +653,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -625,42 +726,42 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.2"
sqflite: sqflite:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqflite name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.4.2+1"
sqflite_android: sqflite_android:
dependency: transitive dependency: transitive
description: description:
name: sqflite_android name: sqflite_android
sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2+2" version: "2.4.2+3"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" sha256: f8a08a13fb8f0f8c590df89d745000bed44a673ed94bac846739e1a016875c21
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.6" version: "2.5.7"
sqflite_common_ffi: sqflite_common_ffi:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqflite_common_ffi name: sqflite_common_ffi
sha256: c59fcdc143839a77581f7a7c4de018e53682408903a0a0800b95ef2dc4033eff sha256: cd0c7f7de39a08f2d54ef144d9058c46eca8461879aaa648025643455c1e5a20
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0+2" version: "2.4.0+3"
sqflite_darwin: sqflite_darwin:
dependency: transitive dependency: transitive
description: description:
@@ -681,10 +782,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqlite3 name: sqlite3
sha256: "00e5e65f8e9b556ed3d999ad310881c956ffb656ed96bea487a4c50ffdff6d14" sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" version: "3.3.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -713,10 +814,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: synchronized name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.0" version: "3.4.0+1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -745,10 +846,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: uuid name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.2" version: "4.5.3"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -761,10 +862,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.2.0"
web: web:
dependency: transitive dependency: transitive
description: description:
@@ -777,10 +878,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: ba7d5750e3441caa1bbe31d9e516348fcf8dfcb32aa29ef87a844a59f4d1f1d0 sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "6.2.0"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:
@@ -814,5 +915,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.11.0 <4.0.0"
flutter: ">=3.38.1" flutter: ">=3.38.4"

View File

@@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: ^3.12.0-35.0.dev sdk: '>=3.10.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions
@@ -31,6 +31,8 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
@@ -56,13 +58,14 @@ 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
http: ^1.6.0 http: ^1.6.0
device_info_plus: ^13.1.0 device_info_plus: ^13.1.0
shared_preferences: ^2.5.5 shared_preferences: ^2.5.5

View File

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

View File

@@ -4,9 +4,11 @@
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
jni
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)