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
+565
View File
@@ -0,0 +1,565 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'workout_controller.dart';
/// Main Workout Tracking Screen
class WorkoutTrackingScreen extends StatefulWidget {
const WorkoutTrackingScreen({Key? key}) : super(key: key);
@override
State<WorkoutTrackingScreen> createState() => _WorkoutTrackingScreenState();
}
class _WorkoutTrackingScreenState extends State<WorkoutTrackingScreen> {
final WorkoutController workoutController = Get.put(WorkoutController());
final MapController mapController = MapController();
final List<Marker> markers = [];
List<LatLng> polylinePoints = [];
@override
void initState() {
super.initState();
}
void _updateMapWithCoordinates() {
if (workoutController.coordinates.isEmpty) return;
final latestCoord = workoutController.coordinates.last;
// Update markers
markers.clear();
markers.add(
Marker(
point: LatLng(latestCoord.latitude, latestCoord.longitude),
width: 80,
height: 80,
child: const Icon(
Icons.location_on,
color: Colors.deepOrange,
size: 32,
),
),
);
// Update polyline
polylinePoints = workoutController.coordinates
.map((c) => LatLng(c.latitude, c.longitude))
.toList();
// Center map on user
mapController.move(
LatLng(latestCoord.latitude, latestCoord.longitude),
15,
);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Workout Tracking'),
backgroundColor: Colors.deepOrange,
elevation: 0,
),
body: GetBuilder<WorkoutController>(
builder: (controller) {
_updateMapWithCoordinates();
return Stack(
children: [
// OpenStreetMap with flutter_map
FlutterMap(
mapController: mapController,
options: MapOptions(
initialCenter: LatLng(37.7749, -122.4194), // San Francisco
initialZoom: 15,
),
children: [
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.fitness.app',
),
PolylineLayer(
polylines: polylinePoints.length > 1
? [
Polyline(
points: polylinePoints,
strokeWidth: 3,
color: const Color(0xFFFF5722),
),
]
: [],
),
MarkerLayer(
markers: markers,
),
],
),
// Statistics Panel
Positioned(
top: 0,
left: 0,
right: 0,
child: _buildStatsPanel(controller),
),
// Control Buttons Panel
Positioned(
bottom: 0,
left: 0,
right: 0,
child: _buildControlPanel(controller),
),
],
);
},
),
);
}
Widget _buildStatsPanel(WorkoutController controller) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Time
Text(
formatTime(controller.elapsedTime),
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.deepOrange,
),
),
const SizedBox(height: 12),
// Distance and Pace
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildStatItem(
label: 'Distance',
value: formatDistance(controller.distance),
icon: Icons.directions_run,
),
Container(
height: 40,
width: 1,
color: Colors.grey.shade300,
),
_buildStatItem(
label: 'Pace',
value: formatPace(controller.currentSpeed),
icon: Icons.speed,
),
],
),
],
),
);
}
Widget _buildStatItem({
required String label,
required String value,
required IconData icon,
}) {
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 16, color: Colors.deepOrange),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
const SizedBox(height: 6),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
],
),
);
}
Widget _buildControlPanel(WorkoutController controller) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 12,
offset: const Offset(0, -2),
),
],
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Workout Type Selector (only available before start)
if (!controller.isTracking)
Container(
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(4),
child: Row(
children: [
_buildWorkoutTypeButton(
controller,
'running',
'🏃 Running',
),
_buildWorkoutTypeButton(
controller,
'walking',
'🚶 Walking',
),
],
),
),
// Main Action Buttons
Row(
children: [
if (!controller.isTracking) ...[
// Start Button
Expanded(
child: ElevatedButton.icon(
onPressed: () => _startWorkout(controller),
icon: const Icon(Icons.play_arrow),
label: const Text('Start'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
] else ...[
// Pause/Resume Button
Expanded(
child: ElevatedButton.icon(
onPressed: () => _togglePause(controller),
icon: Icon(
controller.isPaused ? Icons.play_arrow : Icons.pause),
label: Text(controller.isPaused ? 'Resume' : 'Pause'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
const SizedBox(width: 12),
// Stop Button
Expanded(
child: ElevatedButton.icon(
onPressed: () => _stopAndSubmit(controller),
icon: const Icon(Icons.stop),
label: const Text('Stop'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
],
),
// Loading Indicator for submission
if (controller.isSubmitting) ...[
const SizedBox(height: 16),
const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.deepOrange),
),
),
],
// Warning Message
if (!controller.isTracking && controller.coordinates.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
'Enable location services to start tracking',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
),
],
),
);
}
Widget _buildWorkoutTypeButton(
WorkoutController controller,
String type,
String label,
) {
final isSelected = controller.currentWorkout.workoutType == type;
return Expanded(
child: InkWell(
onTap: () => controller.setWorkoutType(type),
child: Container(
decoration: BoxDecoration(
color: isSelected ? Colors.deepOrange : Colors.transparent,
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(vertical: 10),
child: Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : Colors.grey.shade700,
),
),
),
),
);
}
void _startWorkout(WorkoutController controller) {
controller.startWorkout();
// Map updates will happen through GetBuilder rebuild
_updateMapWithCoordinates();
}
void _togglePause(WorkoutController controller) {
if (controller.isPaused) {
controller.resumeWorkout();
} else {
controller.pauseWorkout();
}
}
void _stopAndSubmit(WorkoutController controller) async {
await controller.stopWorkout();
if (!mounted) return;
// Show workout summary and submission dialog
Get.dialog(
WorkoutSummaryDialog(
controller: controller,
),
barrierDismissible: false,
);
}
@override
void dispose() {
super.dispose();
}
}
/// Workout Summary and Submission Dialog
class WorkoutSummaryDialog extends StatelessWidget {
final WorkoutController controller;
const WorkoutSummaryDialog({
Key? key,
required this.controller,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Workout Complete!',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.deepOrange,
),
),
const SizedBox(height: 20),
// Summary Stats
_buildSummaryRow('Type', controller.currentWorkout.workoutType),
_buildSummaryRow('Distance', formatDistance(controller.distance)),
_buildSummaryRow('Duration', formatTime(controller.elapsedTime)),
_buildSummaryRow(
'Max Speed', '${controller.maxSpeed.toStringAsFixed(2)} m/s'),
_buildSummaryRow(
'Calories',
controller.currentWorkout.duration > 0
? '${(controller.distance / 1000 * (controller.currentWorkout.workoutType == 'running' ? 100 : 50)).toStringAsFixed(0)}'
: '0',
),
const SizedBox(height: 24),
// Action Buttons
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
Get.back(); // Close dialog
},
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey,
side: const BorderSide(color: Colors.grey),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text('Discard'),
),
),
const SizedBox(width: 12),
Expanded(
child: GetBuilder<WorkoutController>(
builder: (ctrl) {
return ElevatedButton(
onPressed: ctrl.isSubmitting
? null
: () => _submitWorkout(controller, context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
disabledBackgroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: ctrl.isSubmitting
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: const Text('Submit Workout'),
);
},
),
),
],
),
],
),
),
),
);
}
Widget _buildSummaryRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
],
),
);
}
Future<void> _submitWorkout(
WorkoutController controller,
BuildContext context,
) async {
// In production, pass actual API endpoint
final success = await controller.submitWorkout(
'https://your-api.com/api/v1/workouts',
);
if (!context.mounted) return;
if (success) {
Get.back(); // Close dialog
Get.snackbar(
'Success',
'Workout submitted successfully!',
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
} else {
Get.snackbar(
'Error',
'Failed to submit workout. Please try again.',
backgroundColor: Colors.red,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
}
}