first commit

This commit is contained in:
Hamza-Ayed
2026-10-04 00:19:45 +03:00
commit 05f1c9ec6b
93 changed files with 10782 additions and 0 deletions
+265
View File
@@ -0,0 +1,265 @@
import 'gps_coordinate.dart';
import 'workout_segment.dart';
/// حالات المزامنة
enum SyncStatus { pending, synced, failed }
/// أنواع التمارين
enum WorkoutType { running, walking }
/// نموذج بيانات التمرين الكامل
/// يحتوي على كل المعلومات المتعلقة بجلسة تمرين واحدة
class Workout {
final String id;
WorkoutType workoutType;
DateTime? startTime;
DateTime? endTime;
double distanceMeters;
int durationSeconds;
double elevationGain;
double elevationLoss;
double calories;
double avgSpeedMps;
double maxSpeedMps;
double avgPaceMinKm;
String? routePolyline;
double? startLat;
double? startLng;
double? endLat;
double? endLng;
String? weather;
double? temperature;
String notes;
SyncStatus syncStatus;
DateTime? syncedAt;
DateTime createdAt;
// In-memory only (not persisted directly)
List<GpsCoordinate> coordinates;
List<WorkoutSegment> segments;
// Tracking state (in-memory only)
bool isPaused;
int totalPausedDurationMs;
DateTime? _pauseStartTime;
Workout({
required this.id,
this.workoutType = WorkoutType.running,
this.startTime,
this.endTime,
this.distanceMeters = 0,
this.durationSeconds = 0,
this.elevationGain = 0,
this.elevationLoss = 0,
this.calories = 0,
this.avgSpeedMps = 0,
this.maxSpeedMps = 0,
this.avgPaceMinKm = 0,
this.routePolyline,
this.startLat,
this.startLng,
this.endLat,
this.endLng,
this.weather,
this.temperature,
this.notes = '',
this.syncStatus = SyncStatus.pending,
this.syncedAt,
DateTime? createdAt,
List<GpsCoordinate>? coordinates,
List<WorkoutSegment>? segments,
this.isPaused = false,
this.totalPausedDurationMs = 0,
}) : createdAt = createdAt ?? DateTime.now(),
coordinates = coordinates ?? [],
segments = segments ?? [];
// ─── Tracking Lifecycle ──────────────────────────────────────────
void start() {
startTime = DateTime.now();
isPaused = false;
totalPausedDurationMs = 0;
_pauseStartTime = null;
}
void pause() {
if (!isPaused) {
isPaused = true;
_pauseStartTime = DateTime.now();
}
}
void resume() {
if (isPaused && _pauseStartTime != null) {
totalPausedDurationMs +=
DateTime.now().difference(_pauseStartTime!).inMilliseconds;
isPaused = false;
_pauseStartTime = null;
}
}
void stop() {
if (isPaused) resume(); // Account for final pause
endTime = DateTime.now();
isPaused = false;
_calculateFinalStats();
}
/// Calculate active duration excluding pauses
int get activeDurationSeconds {
if (startTime == null) return 0;
final end = endTime ?? DateTime.now();
int total = end.difference(startTime!).inSeconds;
int paused = totalPausedDurationMs ~/ 1000;
return (total - paused).clamp(0, total);
}
// ─── Stats Calculation ───────────────────────────────────────────
void _calculateFinalStats() {
durationSeconds = activeDurationSeconds;
if (durationSeconds > 0 && distanceMeters > 0) {
avgSpeedMps = distanceMeters / durationSeconds;
// Pace in min/km
double kmPerHour = avgSpeedMps * 3.6;
if (kmPerHour > 0) {
avgPaceMinKm = 60.0 / kmPerHour;
}
}
// Calories estimation (MET-based for 70kg person)
calories = _estimateCalories();
// Elevation from coordinates
_calculateElevation();
}
double _estimateCalories() {
// MET values: Running ~10, Walking ~3.8
double met = workoutType == WorkoutType.running ? 10.0 : 3.8;
double weightKg = 70.0; // Default weight
double hours = durationSeconds / 3600.0;
return met * weightKg * hours;
}
void _calculateElevation() {
if (coordinates.length < 2) return;
double gain = 0;
double loss = 0;
for (int i = 1; i < coordinates.length; i++) {
final prevAlt = coordinates[i - 1].altitude;
final currAlt = coordinates[i].altitude;
if (prevAlt != null && currAlt != null) {
double diff = currAlt - prevAlt;
// Filter noise: ignore changes < 1m
if (diff > 1.0) {
gain += diff;
} else if (diff < -1.0) {
loss += diff.abs();
}
}
}
elevationGain = gain;
elevationLoss = loss;
}
// ─── Serialization ──────────────────────────────────────────────
Map<String, dynamic> toMap() {
return {
'id': id,
'workout_type': workoutType.name,
'start_time': startTime?.toIso8601String(),
'end_time': endTime?.toIso8601String(),
'distance_meters': distanceMeters,
'duration_seconds': durationSeconds,
'elevation_gain': elevationGain,
'elevation_loss': elevationLoss,
'calories': calories,
'avg_speed_mps': avgSpeedMps,
'max_speed_mps': maxSpeedMps,
'avg_pace_min_km': avgPaceMinKm,
'route_polyline': routePolyline,
'start_lat': startLat,
'start_lng': startLng,
'end_lat': endLat,
'end_lng': endLng,
'weather': weather,
'temperature': temperature,
'notes': notes,
'sync_status': syncStatus.name,
'synced_at': syncedAt?.toIso8601String(),
'created_at': createdAt.toIso8601String(),
};
}
factory Workout.fromMap(Map<String, dynamic> map) {
return Workout(
id: map['id'] as String,
workoutType: WorkoutType.values.firstWhere(
(e) => e.name == map['workout_type'],
orElse: () => WorkoutType.running,
),
startTime: map['start_time'] != null
? DateTime.parse(map['start_time'] as String)
: null,
endTime: map['end_time'] != null
? DateTime.parse(map['end_time'] as String)
: null,
distanceMeters: (map['distance_meters'] as num?)?.toDouble() ?? 0,
durationSeconds: (map['duration_seconds'] as num?)?.toInt() ?? 0,
elevationGain: (map['elevation_gain'] as num?)?.toDouble() ?? 0,
elevationLoss: (map['elevation_loss'] as num?)?.toDouble() ?? 0,
calories: (map['calories'] as num?)?.toDouble() ?? 0,
avgSpeedMps: (map['avg_speed_mps'] as num?)?.toDouble() ?? 0,
maxSpeedMps: (map['max_speed_mps'] as num?)?.toDouble() ?? 0,
avgPaceMinKm: (map['avg_pace_min_km'] as num?)?.toDouble() ?? 0,
routePolyline: map['route_polyline'] as String?,
startLat: (map['start_lat'] as num?)?.toDouble(),
startLng: (map['start_lng'] as num?)?.toDouble(),
endLat: (map['end_lat'] as num?)?.toDouble(),
endLng: (map['end_lng'] as num?)?.toDouble(),
weather: map['weather'] as String?,
temperature: (map['temperature'] as num?)?.toDouble(),
notes: map['notes'] as String? ?? '',
syncStatus: SyncStatus.values.firstWhere(
(e) => e.name == map['sync_status'],
orElse: () => SyncStatus.pending,
),
syncedAt: map['synced_at'] != null
? DateTime.parse(map['synced_at'] as String)
: null,
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'] as String)
: DateTime.now(),
);
}
/// Convert to API payload for server submission
Map<String, dynamic> toApiPayload() {
return {
'workout_type': workoutType.name,
'distance_meters': distanceMeters.toInt(),
'duration_seconds': durationSeconds,
'elevation_gain_meters': elevationGain.toInt(),
'elevation_loss_meters': elevationLoss.toInt(),
'calories_burned': calories,
'max_speed_mps': maxSpeedMps,
'route_polyline': routePolyline ?? '',
'start_time': startTime?.toUtc().toIso8601String() ?? '',
'end_time': endTime?.toUtc().toIso8601String() ?? '',
'weather_condition': weather ?? '',
'temperature_celsius': temperature ?? 0,
'notes': notes,
'is_public': false,
'segments': segments.map((s) => s.toApiMap()).toList(),
};
}
}