branch backend + ajout du serveur backend, next, et sqlite

This commit is contained in:
streaper2
2026-04-29 15:45:09 +02:00
parent 105eb1cab0
commit cea2fab989
42 changed files with 10566 additions and 4 deletions

View File

@@ -0,0 +1,146 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:device_info_plus/device_info_plus.dart';
import 'package:crypto/crypto.dart';
import '../data/models/shot.dart';
import '../data/models/target_type.dart';
import 'wallet_identity_service.dart';
class AiExportService {
// Utilise 10.0.2.2 pour l'émulateur Android, sinon localhost.
// Pour un appareil physique, il faudra utiliser l'IP locale du PC (ex: 192.168.1.X).
static String get _defaultApiUrl {
if (Platform.isAndroid) {
return 'http://10.0.2.2:3000/api/upload';
}
return 'http://localhost:3000/api/upload';
}
/// Extrait les informations de l'appareil
Future<Map<String, dynamic>> _getDeviceInfo() async {
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
Map<String, dynamic> deviceData = {'model': 'Unknown', 'os': 'Unknown'};
try {
if (Platform.isAndroid) {
final androidInfo = await deviceInfoPlugin.androidInfo;
deviceData['model'] = '${androidInfo.brand} ${androidInfo.model}';
deviceData['os'] = 'Android ${androidInfo.version.release}';
} else if (Platform.isIOS) {
final iosInfo = await deviceInfoPlugin.iosInfo;
deviceData['model'] = iosInfo.name;
deviceData['os'] = '${iosInfo.systemName} ${iosInfo.systemVersion}';
} else if (Platform.isWindows) {
final windowsInfo = await deviceInfoPlugin.windowsInfo;
deviceData['model'] = 'Windows PC';
deviceData['os'] = 'Windows ${windowsInfo.majorVersion}.${windowsInfo.minorVersion}';
}
} catch (e) {
print('Erreur lors de la récupération des infos appareil: $e');
}
return deviceData;
}
/// Exporte l'image et les données de plotting vers le serveur
Future<bool> exportData({
required String imagePath,
required String sessionId,
required TargetType targetType,
required double targetCenterX,
required double targetCenterY,
required double targetRadius,
required List<Shot> shots,
String? apiUrl,
}) async {
try {
final url = Uri.parse(apiUrl ?? _defaultApiUrl);
final request = http.MultipartRequest('POST', url);
// 1. Prepare image
final file = File(imagePath);
if (!await file.exists()) {
throw Exception('Le fichier image n\'existe pas');
}
// Read image metadata (approximate dimensions since decoding image can be heavy)
// On the frontend we usually have aspectRatio, here we use generic values if not available.
final deviceData = await _getDeviceInfo();
// We approximate the target corners from center and radius
// radius is relative (0 to 1). We need image width/height to get pixels.
// But we can just pass relative corners as well, or a normalized bounding box.
// Let's create normalized corners (0 to 1).
final corners = [
{"norm_x": targetCenterX - targetRadius, "norm_y": targetCenterY - targetRadius},
{"norm_x": targetCenterX + targetRadius, "norm_y": targetCenterY - targetRadius},
{"norm_x": targetCenterX + targetRadius, "norm_y": targetCenterY + targetRadius},
{"norm_x": targetCenterX - targetRadius, "norm_y": targetCenterY + targetRadius},
];
// Format the impacts
final formattedImpacts = shots.asMap().entries.map((entry) {
final index = entry.key;
final shot = entry.value;
return {
"id": index + 1,
"label": "bullet_hole",
"score": shot.score,
"coords": {
"norm_x": shot.x,
"norm_y": shot.y
}
};
}).toList();
// Get and hash the wallet identity
final walletService = WalletIdentityService();
final phrase = await walletService.getIdentityPhrase();
final phraseBytes = utf8.encode(phrase);
final walletHash = sha256.convert(phraseBytes).toString();
// Build JSON payload
final plottingJson = {
"session_id": sessionId,
"wallet_hash": walletHash,
"timestamp": DateTime.now().toIso8601String(),
"device_info": deviceData,
"target_metadata": {
"type": targetType.name,
"distance_meters": 25, // Default/placeholder
"weapon": "Unknown", // Default/placeholder
// The backend could extract exact width/height from the image.
},
"plotting": {
"target_corners": corners,
"impacts": formattedImpacts
}
};
// Add fields to request
request.fields['plotting'] = jsonEncode(plottingJson);
// Add file
request.files.add(
await http.MultipartFile.fromPath('photo', imagePath),
);
// Send request
final response = await request.send();
if (response.statusCode == 200) {
final responseData = await response.stream.bytesToString();
print('Export réussi: $responseData');
return true;
} else {
final errorData = await response.stream.bytesToString();
print('Erreur d\'export: ${response.statusCode} - $errorData');
return false;
}
} catch (e) {
print('Exception lors de l\'export: $e');
return false;
}
}
}