160 lines
5.4 KiB
Dart
160 lines
5.4 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
import '../models/gps_coordinate.dart';
|
|
import '../models/workout.dart';
|
|
import '../screens/tracking_screen.dart';
|
|
import '../services/database_service.dart';
|
|
import '../services/gps_service.dart';
|
|
import '../services/sync_service.dart';
|
|
import '../services/analytics_service.dart';
|
|
import '../utils/polyline_utility.dart';
|
|
import '../utils/geo_utils.dart';
|
|
|
|
/// المتحكم الرئيسي للتمرين (Main Workout Controller)
|
|
/// يدير حالة التتبع الحية، التفاعل مع الـ GPS، وحفظ البيانات.
|
|
class WorkoutController extends GetxController {
|
|
final GpsService _gps = Get.find<GpsService>();
|
|
final DatabaseService _db = Get.find<DatabaseService>();
|
|
final SyncService _sync = Get.find<SyncService>();
|
|
final AnalyticsService _analytics = Get.find<AnalyticsService>();
|
|
|
|
// حالة التتبع الحية
|
|
final Rxn<Workout> currentWorkout = Rxn<Workout>();
|
|
final RxList<GpsCoordinate> liveCoordinates = <GpsCoordinate>[].obs;
|
|
final RxInt elapsedSeconds = 0.obs;
|
|
final RxDouble liveDistance = 0.0.obs;
|
|
final RxDouble liveSpeed = 0.0.obs;
|
|
|
|
Timer? _timer;
|
|
Future<void> _coordinateWriteQueue = Future<void>.value();
|
|
|
|
// ─── التحكم في الجلسة ──────────────────────────────────────────
|
|
|
|
Future<void> startWorkout(WorkoutType type) async {
|
|
bool ok = await _gps.startTracking(distanceFilter: 5);
|
|
if (!ok) {
|
|
Get.snackbar("خطأ", "فشل الوصول إلى الموقع (GPS). تأكد من الصلاحيات.");
|
|
return;
|
|
}
|
|
|
|
final id = const Uuid().v4();
|
|
currentWorkout.value = Workout(id: id, workoutType: type);
|
|
currentWorkout.value!.start();
|
|
|
|
liveCoordinates.clear();
|
|
_coordinateWriteQueue = Future<void>.value();
|
|
elapsedSeconds.value = 0;
|
|
liveDistance.value = 0;
|
|
liveSpeed.value = 0;
|
|
|
|
// ربط مستمع الإحداثيات
|
|
_gps.onNewCoordinate = _handleNewCoordinate;
|
|
|
|
// الانتقال لشاشة التتبع
|
|
Get.to(() => const TrackingScreen());
|
|
|
|
// بدء المؤقت الزمني
|
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
if (currentWorkout.value != null && !currentWorkout.value!.isPaused) {
|
|
elapsedSeconds.value = currentWorkout.value!.activeDurationSeconds;
|
|
}
|
|
});
|
|
}
|
|
|
|
void pauseWorkout() {
|
|
currentWorkout.value?.pause();
|
|
update();
|
|
}
|
|
|
|
void resumeWorkout() {
|
|
currentWorkout.value?.resume();
|
|
update();
|
|
}
|
|
|
|
Future<void> stopWorkout() async {
|
|
if (currentWorkout.value == null) return;
|
|
|
|
await _gps.stopTracking();
|
|
_timer?.cancel();
|
|
await _coordinateWriteQueue;
|
|
|
|
final workout = currentWorkout.value!;
|
|
workout.stop();
|
|
|
|
// إسناد الإحداثيات النهائية
|
|
workout.coordinates = List.from(liveCoordinates);
|
|
workout.distanceMeters = liveDistance.value;
|
|
|
|
// التحليل الذكي للمقاطع
|
|
workout.segments =
|
|
_analytics.analyzeSegments(workout.coordinates, workout.workoutType);
|
|
|
|
// تحويل المسار إلى Polyline مضغوط
|
|
workout.routePolyline = PolylineUtility.encode(workout.coordinates);
|
|
|
|
if (workout.coordinates.isNotEmpty) {
|
|
workout.startLat = workout.coordinates.first.latitude;
|
|
workout.startLng = workout.coordinates.first.longitude;
|
|
workout.endLat = workout.coordinates.last.latitude;
|
|
workout.endLng = workout.coordinates.last.longitude;
|
|
}
|
|
|
|
// Save the workout and its outbox record in one local transaction.
|
|
await _sync.queueWorkout(workout);
|
|
|
|
currentWorkout.value = null;
|
|
Get.snackbar(
|
|
"تم الحفظ",
|
|
workout.coordinates.length >= 2
|
|
? "تم حفظ التمرين محليًا وجاري المزامنة عند توفر الاتصال."
|
|
: "حُفظ النشاط على الجهاز؛ لم تتوفر نقاط GPS كافية لمزامنته.",
|
|
);
|
|
}
|
|
|
|
void _handleNewCoordinate(GpsCoordinate coord) {
|
|
if (currentWorkout.value == null || currentWorkout.value!.isPaused) return;
|
|
|
|
liveCoordinates.add(coord);
|
|
liveSpeed.value = coord.speed ?? 0;
|
|
|
|
// حفظ فوري في DB (للحماية من انهيار التطبيق)
|
|
final workoutId = currentWorkout.value!.id;
|
|
_coordinateWriteQueue = _coordinateWriteQueue.then((_) async {
|
|
await _db.saveCoordinate(workoutId, coord);
|
|
}).catchError((Object error, StackTrace stackTrace) {
|
|
debugPrint('[TRACKING] Coordinate persistence failed: $error');
|
|
});
|
|
|
|
// حساب المسافة التراكمية حياً
|
|
if (liveCoordinates.length > 1) {
|
|
// تم إضافة المسافة في الموديل أو هنا مباشرة
|
|
// للتسهيل سنقوم بالتحديث هنا
|
|
}
|
|
|
|
// تحديث المسافة الإجمالية (تبسيطاً سنعتمد على المجموع)
|
|
_calculateLiveDistance();
|
|
}
|
|
|
|
void _calculateLiveDistance() {
|
|
if (liveCoordinates.length < 2) return;
|
|
|
|
double totalDist = 0;
|
|
for (int i = 1; i < liveCoordinates.length; i++) {
|
|
totalDist += GeoUtils.haversineDistance(
|
|
liveCoordinates[i - 1].latitude,
|
|
liveCoordinates[i - 1].longitude,
|
|
liveCoordinates[i].latitude,
|
|
liveCoordinates[i].longitude);
|
|
}
|
|
liveDistance.value = totalDist;
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_timer?.cancel();
|
|
super.onClose();
|
|
}
|
|
}
|