Base de dados atualizada
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:google_maps_flutter/google_maps_flutter.dart';
|
|||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import '../constants/app_colors.dart';
|
import '../constants/app_colors.dart';
|
||||||
import '../constants/app_strings.dart';
|
import '../constants/app_strings.dart';
|
||||||
|
import '../services/supabase_service.dart';
|
||||||
|
|
||||||
class GoogleMapScreen extends StatefulWidget {
|
class GoogleMapScreen extends StatefulWidget {
|
||||||
const GoogleMapScreen({super.key});
|
const GoogleMapScreen({super.key});
|
||||||
@@ -49,7 +50,11 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
await _initTracking();
|
await _initTracking();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<BitmapDescriptor> _createArrowMarker(Color color, Color borderColor, double size) async {
|
Future<BitmapDescriptor> _createArrowMarker(
|
||||||
|
Color color,
|
||||||
|
Color borderColor,
|
||||||
|
double size,
|
||||||
|
) async {
|
||||||
final ui.PictureRecorder recorder = ui.PictureRecorder();
|
final ui.PictureRecorder recorder = ui.PictureRecorder();
|
||||||
final Canvas canvas = Canvas(recorder);
|
final Canvas canvas = Canvas(recorder);
|
||||||
final Path path = Path();
|
final Path path = Path();
|
||||||
@@ -60,17 +65,29 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
path.close();
|
path.close();
|
||||||
|
|
||||||
canvas.drawPath(
|
canvas.drawPath(
|
||||||
path.shift(const Offset(0, 2)),
|
path.shift(const Offset(0, 2)),
|
||||||
Paint()
|
Paint()
|
||||||
..color = Colors.black38
|
..color = Colors.black38
|
||||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2));
|
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2),
|
||||||
|
);
|
||||||
canvas.drawPath(path, Paint()..color = color);
|
canvas.drawPath(path, Paint()..color = color);
|
||||||
|
|
||||||
double strokeWidth = size * 0.12;
|
double strokeWidth = size * 0.12;
|
||||||
canvas.drawPath(path, Paint()..color = borderColor..style = ui.PaintingStyle.stroke..strokeWidth = strokeWidth);
|
canvas.drawPath(
|
||||||
|
path,
|
||||||
|
Paint()
|
||||||
|
..color = borderColor
|
||||||
|
..style = ui.PaintingStyle.stroke
|
||||||
|
..strokeWidth = strokeWidth,
|
||||||
|
);
|
||||||
|
|
||||||
final ui.Image image = await recorder.endRecording().toImage(size.toInt(), size.toInt() + 4);
|
final ui.Image image = await recorder.endRecording().toImage(
|
||||||
final ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
size.toInt(),
|
||||||
|
size.toInt() + 4,
|
||||||
|
);
|
||||||
|
final ByteData? byteData = await image.toByteData(
|
||||||
|
format: ui.ImageByteFormat.png,
|
||||||
|
);
|
||||||
return BitmapDescriptor.bytes(byteData!.buffer.asUint8List());
|
return BitmapDescriptor.bytes(byteData!.buffer.asUint8List());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +107,9 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
permission = await Geolocator.requestPermission();
|
permission = await Geolocator.requestPermission();
|
||||||
if (permission == LocationPermission.denied) return;
|
if (permission == LocationPermission.denied) return;
|
||||||
}
|
}
|
||||||
Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.bestForNavigation);
|
Position position = await Geolocator.getCurrentPosition(
|
||||||
|
desiredAccuracy: LocationAccuracy.bestForNavigation,
|
||||||
|
);
|
||||||
_currentPosition = LatLng(position.latitude, position.longitude);
|
_currentPosition = LatLng(position.latitude, position.longitude);
|
||||||
_routePoints.add(_currentPosition);
|
_routePoints.add(_currentPosition);
|
||||||
|
|
||||||
@@ -100,15 +119,23 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _startLocationStream() {
|
void _startLocationStream() {
|
||||||
_positionStreamSubscription = Geolocator.getPositionStream(
|
_positionStreamSubscription =
|
||||||
locationSettings: const LocationSettings(accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 1)
|
Geolocator.getPositionStream(
|
||||||
).listen((Position position) {
|
locationSettings: const LocationSettings(
|
||||||
final now = DateTime.now();
|
accuracy: LocationAccuracy.bestForNavigation,
|
||||||
if (_lastUpdate == null || now.difference(_lastUpdate!).inMilliseconds > 2000) {
|
distanceFilter: 1,
|
||||||
_lastUpdate = now;
|
),
|
||||||
_updatePosition(LatLng(position.latitude, position.longitude), position.speed);
|
).listen((Position position) {
|
||||||
}
|
final now = DateTime.now();
|
||||||
});
|
if (_lastUpdate == null ||
|
||||||
|
now.difference(_lastUpdate!).inMilliseconds > 2000) {
|
||||||
|
_lastUpdate = now;
|
||||||
|
_updatePosition(
|
||||||
|
LatLng(position.latitude, position.longitude),
|
||||||
|
position.speed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updatePosition(LatLng newPoint, double speed) {
|
void _updatePosition(LatLng newPoint, double speed) {
|
||||||
@@ -116,19 +143,21 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
double currentSpeedKmh = speed * 3.6;
|
double currentSpeedKmh = speed * 3.6;
|
||||||
if (currentSpeedKmh < 0) currentSpeedKmh = 0;
|
if (currentSpeedKmh < 0) currentSpeedKmh = 0;
|
||||||
|
|
||||||
_currentSpeed = currentSpeedKmh;
|
_currentSpeed = currentSpeedKmh;
|
||||||
|
|
||||||
if (_isRunning) {
|
if (_isRunning) {
|
||||||
if (currentSpeedKmh > _maxSpeed) {
|
if (currentSpeedKmh > _maxSpeed) {
|
||||||
_maxSpeed = currentSpeedKmh;
|
_maxSpeed = currentSpeedKmh;
|
||||||
}
|
}
|
||||||
|
|
||||||
_totalDistance += Geolocator.distanceBetween(
|
_totalDistance += Geolocator.distanceBetween(
|
||||||
_currentPosition.latitude, _currentPosition.longitude,
|
_currentPosition.latitude,
|
||||||
newPoint.latitude, newPoint.longitude
|
_currentPosition.longitude,
|
||||||
|
newPoint.latitude,
|
||||||
|
newPoint.longitude,
|
||||||
);
|
);
|
||||||
|
|
||||||
_routePoints.add(newPoint);
|
_routePoints.add(newPoint);
|
||||||
_updateTraveledPolylines();
|
_updateTraveledPolylines();
|
||||||
} else {
|
} else {
|
||||||
@@ -148,42 +177,53 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
void _updateTraveledPolylines() {
|
void _updateTraveledPolylines() {
|
||||||
if (_routePoints.length > 1) {
|
if (_routePoints.length > 1) {
|
||||||
_polylines.clear();
|
_polylines.clear();
|
||||||
_polylines.add(Polyline(
|
_polylines.add(
|
||||||
polylineId: const PolylineId('route_glow'),
|
Polyline(
|
||||||
points: List.from(_routePoints),
|
polylineId: const PolylineId('route_glow'),
|
||||||
color: AppColors.coral.withOpacity(0.3),
|
points: List.from(_routePoints),
|
||||||
width: 12,
|
color: AppColors.coral.withOpacity(0.3),
|
||||||
zIndex: 9
|
width: 12,
|
||||||
));
|
zIndex: 9,
|
||||||
_polylines.add(Polyline(
|
),
|
||||||
polylineId: const PolylineId('route'),
|
);
|
||||||
points: List.from(_routePoints),
|
_polylines.add(
|
||||||
color: AppColors.coral,
|
Polyline(
|
||||||
width: 5,
|
polylineId: const PolylineId('route'),
|
||||||
jointType: JointType.round,
|
points: List.from(_routePoints),
|
||||||
zIndex: 10
|
color: AppColors.coral,
|
||||||
));
|
width: 5,
|
||||||
|
jointType: JointType.round,
|
||||||
|
zIndex: 10,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateMarkers() {
|
void _updateMarkers() {
|
||||||
_markers.clear();
|
_markers.clear();
|
||||||
_markers.add(Marker(
|
_markers.add(
|
||||||
|
Marker(
|
||||||
markerId: const MarkerId('follower'),
|
markerId: const MarkerId('follower'),
|
||||||
position: _currentPosition,
|
position: _currentPosition,
|
||||||
rotation: _calculateRotation(_routePoints),
|
rotation: _calculateRotation(_routePoints),
|
||||||
flat: true,
|
flat: true,
|
||||||
anchor: const Offset(0.5, 0.5),
|
anchor: const Offset(0.5, 0.5),
|
||||||
icon: _arrowIcon ?? BitmapDescriptor.defaultMarker,
|
icon: _arrowIcon ?? BitmapDescriptor.defaultMarker,
|
||||||
zIndex: 12
|
zIndex: 12,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double _calculateRotation(List<LatLng> points) {
|
double _calculateRotation(List<LatLng> points) {
|
||||||
if (points.length < 2) return 0;
|
if (points.length < 2) return 0;
|
||||||
LatLng p1 = points[points.length - 2];
|
LatLng p1 = points[points.length - 2];
|
||||||
LatLng p2 = points.last;
|
LatLng p2 = points.last;
|
||||||
return Geolocator.bearingBetween(p1.latitude, p1.longitude, p2.latitude, p2.longitude);
|
return Geolocator.bearingBetween(
|
||||||
|
p1.latitude,
|
||||||
|
p1.longitude,
|
||||||
|
p2.latitude,
|
||||||
|
p2.longitude,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showStartConfirmation() {
|
void _showStartConfirmation() {
|
||||||
@@ -192,17 +232,43 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: AppColors.backgroundGrey,
|
backgroundColor: AppColors.backgroundGrey,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
|
||||||
title: Text(AppStrings.startRunQuestion, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
title: Text(
|
||||||
content: Text(AppStrings.startRunDescription, style: const TextStyle(color: Colors.white70)),
|
AppStrings.startRunQuestion,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
AppStrings.startRunDescription,
|
||||||
|
style: const TextStyle(color: Colors.white70),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: Text(AppStrings.cancel, style: const TextStyle(color: Colors.white54))),
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(
|
||||||
|
AppStrings.cancel,
|
||||||
|
style: const TextStyle(color: Colors.white54),
|
||||||
|
),
|
||||||
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_startCountdown();
|
_startCountdown();
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.coral, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
|
style: ElevatedButton.styleFrom(
|
||||||
child: Text(AppStrings.yes, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
backgroundColor: AppColors.coral,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
AppStrings.yes,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -253,6 +319,12 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
_isRunning = false;
|
_isRunning = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Calculate pace (minutes per kilometer)
|
||||||
|
final pace = _calculatePace(finalDistance, finalTime);
|
||||||
|
|
||||||
|
// Save run data to database
|
||||||
|
_saveRunData(finalDistance, pace, finalTime);
|
||||||
|
|
||||||
showGeneralDialog(
|
showGeneralDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
@@ -266,7 +338,10 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(35), side: const BorderSide(color: Colors.white10, width: 2)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(35),
|
||||||
|
side: const BorderSide(color: Colors.white10, width: 2),
|
||||||
|
),
|
||||||
content: Container(
|
content: Container(
|
||||||
width: MediaQuery.of(context).size.width * 0.85,
|
width: MediaQuery.of(context).size.width * 0.85,
|
||||||
padding: const EdgeInsets.all(25),
|
padding: const EdgeInsets.all(25),
|
||||||
@@ -275,11 +350,26 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(15),
|
padding: const EdgeInsets.all(15),
|
||||||
decoration: const BoxDecoration(color: AppColors.coral, shape: BoxShape.circle),
|
decoration: const BoxDecoration(
|
||||||
child: const Icon(Icons.emoji_events_rounded, color: Colors.white, size: 40),
|
color: AppColors.coral,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.emoji_events_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(AppStrings.runFinished, style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w900, letterSpacing: 1)),
|
Text(
|
||||||
|
AppStrings.runFinished,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
Container(
|
Container(
|
||||||
height: 180,
|
height: 180,
|
||||||
@@ -291,18 +381,34 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(23),
|
borderRadius: BorderRadius.circular(23),
|
||||||
child: GoogleMap(
|
child: GoogleMap(
|
||||||
initialCameraPosition: CameraPosition(target: finalRoute.isNotEmpty ? finalRoute.first : _currentPosition, zoom: 15),
|
initialCameraPosition: CameraPosition(
|
||||||
|
target: finalRoute.isNotEmpty
|
||||||
|
? finalRoute.first
|
||||||
|
: _currentPosition,
|
||||||
|
zoom: 15,
|
||||||
|
),
|
||||||
onMapCreated: (controller) {
|
onMapCreated: (controller) {
|
||||||
if (finalRoute.isNotEmpty) {
|
if (finalRoute.isNotEmpty) {
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
Future.delayed(
|
||||||
controller.animateCamera(CameraUpdate.newLatLngBounds(
|
const Duration(milliseconds: 500),
|
||||||
_getBounds(finalRoute), 40
|
() {
|
||||||
));
|
controller.animateCamera(
|
||||||
});
|
CameraUpdate.newLatLngBounds(
|
||||||
|
_getBounds(finalRoute),
|
||||||
|
40,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
polylines: {
|
polylines: {
|
||||||
Polyline(polylineId: const PolylineId('final_path'), points: finalRoute, color: AppColors.coral, width: 4)
|
Polyline(
|
||||||
|
polylineId: const PolylineId('final_path'),
|
||||||
|
points: finalRoute,
|
||||||
|
color: AppColors.coral,
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
zoomControlsEnabled: false,
|
zoomControlsEnabled: false,
|
||||||
myLocationButtonEnabled: false,
|
myLocationButtonEnabled: false,
|
||||||
@@ -313,11 +419,20 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 25),
|
const SizedBox(height: 25),
|
||||||
_buildResultRow(AppStrings.totalDistance, _formatDistance(finalDistance)),
|
_buildResultRow(
|
||||||
|
AppStrings.totalDistance,
|
||||||
|
_formatDistance(finalDistance),
|
||||||
|
),
|
||||||
const Divider(color: Colors.white10, height: 25),
|
const Divider(color: Colors.white10, height: 25),
|
||||||
_buildResultRow(AppStrings.totalTime, _formatTime(finalTime)),
|
_buildResultRow(
|
||||||
|
AppStrings.totalTime,
|
||||||
|
_formatTime(finalTime),
|
||||||
|
),
|
||||||
const Divider(color: Colors.white10, height: 25),
|
const Divider(color: Colors.white10, height: 25),
|
||||||
_buildResultRow(AppStrings.maxSpeed, "${finalMaxSpeed.toStringAsFixed(1)} ${AppStrings.kmhUnit}"),
|
_buildResultRow(
|
||||||
|
AppStrings.maxSpeed,
|
||||||
|
"${finalMaxSpeed.toStringAsFixed(1)} ${AppStrings.kmhUnit}",
|
||||||
|
),
|
||||||
const SizedBox(height: 35),
|
const SizedBox(height: 35),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -334,10 +449,18 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
foregroundColor: AppColors.background,
|
foregroundColor: AppColors.background,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
child: Text(AppStrings.close.toUpperCase(), style: const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 2)),
|
child: Text(
|
||||||
|
AppStrings.close.toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -365,7 +488,8 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _formatDistance(double meters) {
|
String _formatDistance(double meters) {
|
||||||
if (meters < 1000) return "${meters.toStringAsFixed(0)} ${AppStrings.metersUnit}";
|
if (meters < 1000)
|
||||||
|
return "${meters.toStringAsFixed(0)} ${AppStrings.metersUnit}";
|
||||||
return "${(meters / 1000).toStringAsFixed(2)} ${AppStrings.kmUnit}";
|
return "${(meters / 1000).toStringAsFixed(2)} ${AppStrings.kmUnit}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,72 +503,157 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
return "${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}";
|
return "${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate pace (minutes per kilometer)
|
||||||
|
double _calculatePace(double distanceInMeters, int timeInSeconds) {
|
||||||
|
if (distanceInMeters <= 0 || timeInSeconds <= 0) return 0.0;
|
||||||
|
|
||||||
|
double distanceInKm = distanceInMeters / 1000;
|
||||||
|
double paceInMinutesPerKm = timeInSeconds / 60.0 / distanceInKm;
|
||||||
|
|
||||||
|
return paceInMinutesPerKm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save run data to database
|
||||||
|
Future<void> _saveRunData(double distance, double pace, int duration) async {
|
||||||
|
try {
|
||||||
|
await SupabaseService.saveRun(
|
||||||
|
distance: distance,
|
||||||
|
pace: pace,
|
||||||
|
duration: duration,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show success message (optional)
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Corrida salva com sucesso!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Show error message
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erro ao salvar corrida: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
_isRunning ? AppStrings.mapTitleRunning : AppStrings.appTitle.toUpperCase(),
|
_isRunning
|
||||||
style: const TextStyle(fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 2, fontSize: 18)
|
? AppStrings.mapTitleRunning
|
||||||
),
|
: AppStrings.appTitle.toUpperCase(),
|
||||||
centerTitle: true,
|
style: const TextStyle(
|
||||||
backgroundColor: Colors.transparent,
|
fontWeight: FontWeight.w900,
|
||||||
elevation: 0,
|
color: Colors.white,
|
||||||
|
letterSpacing: 2,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||||
onPressed: () => Navigator.pop(context)
|
onPressed: () => Navigator.pop(context),
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: MediaQuery.of(context).size.width * 0.94,
|
width: MediaQuery.of(context).size.width * 0.94,
|
||||||
height: MediaQuery.of(context).size.height * 0.72,
|
height: MediaQuery.of(context).size.height * 0.72,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.backgroundGrey,
|
color: AppColors.backgroundGrey,
|
||||||
borderRadius: BorderRadius.circular(55),
|
borderRadius: BorderRadius.circular(55),
|
||||||
border: Border.all(color: Colors.white10, width: 3),
|
border: Border.all(color: Colors.white10, width: 3),
|
||||||
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 40, spreadRadius: -10)]
|
boxShadow: const [
|
||||||
),
|
BoxShadow(
|
||||||
child: ClipRRect(
|
color: Colors.black54,
|
||||||
borderRadius: BorderRadius.circular(52),
|
blurRadius: 40,
|
||||||
child: _isLoading
|
spreadRadius: -10,
|
||||||
? const Center(child: CircularProgressIndicator(color: AppColors.coral))
|
),
|
||||||
: GoogleMap(
|
],
|
||||||
initialCameraPosition: CameraPosition(target: _currentPosition, zoom: 17.5),
|
),
|
||||||
onMapCreated: (controller) => _mapController = controller,
|
child: ClipRRect(
|
||||||
markers: _markers,
|
borderRadius: BorderRadius.circular(52),
|
||||||
polylines: _polylines,
|
child: _isLoading
|
||||||
zoomControlsEnabled: false,
|
? const Center(
|
||||||
myLocationButtonEnabled: false,
|
child: CircularProgressIndicator(
|
||||||
compassEnabled: false,
|
color: AppColors.coral,
|
||||||
mapToolbarEnabled: false,
|
),
|
||||||
)
|
)
|
||||||
)
|
: GoogleMap(
|
||||||
|
initialCameraPosition: CameraPosition(
|
||||||
|
target: _currentPosition,
|
||||||
|
zoom: 17.5,
|
||||||
|
),
|
||||||
|
onMapCreated: (controller) =>
|
||||||
|
_mapController = controller,
|
||||||
|
markers: _markers,
|
||||||
|
polylines: _polylines,
|
||||||
|
zoomControlsEnabled: false,
|
||||||
|
myLocationButtonEnabled: false,
|
||||||
|
compassEnabled: false,
|
||||||
|
mapToolbarEnabled: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 115, left: 25, right: 25,
|
top: 115,
|
||||||
|
left: 25,
|
||||||
|
right: 25,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 22, horizontal: 10),
|
padding: const EdgeInsets.symmetric(vertical: 22, horizontal: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.background.withOpacity(0.9),
|
color: AppColors.background.withOpacity(0.9),
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
border: Border.all(color: Colors.white10),
|
border: Border.all(color: Colors.white10),
|
||||||
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.3), blurRadius: 15)]
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
blurRadius: 15,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
_buildStat(AppStrings.mapPace, _currentSpeed.toStringAsFixed(1), AppStrings.kmhUnit),
|
_buildStat(
|
||||||
|
AppStrings.mapPace,
|
||||||
|
_currentSpeed.toStringAsFixed(1),
|
||||||
|
AppStrings.kmhUnit,
|
||||||
|
),
|
||||||
_buildDivider(),
|
_buildDivider(),
|
||||||
_buildStat(AppStrings.mapRoute, _formatDistanceValue(_totalDistance), _totalDistance < 1000 ? AppStrings.metersUnit : AppStrings.kmUnit),
|
_buildStat(
|
||||||
|
AppStrings.mapRoute,
|
||||||
|
_formatDistanceValue(_totalDistance),
|
||||||
|
_totalDistance < 1000
|
||||||
|
? AppStrings.metersUnit
|
||||||
|
: AppStrings.kmUnit,
|
||||||
|
),
|
||||||
_buildDivider(),
|
_buildDivider(),
|
||||||
_buildStat(AppStrings.mapTime, _formatTimeShort(_secondsElapsed), ""),
|
_buildStat(
|
||||||
|
AppStrings.mapTime,
|
||||||
|
_formatTimeShort(_secondsElapsed),
|
||||||
|
"",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -459,30 +668,58 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(AppStrings.prepare, style: const TextStyle(color: Colors.white54, fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 5)),
|
Text(
|
||||||
|
AppStrings.prepare,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white54,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 5,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text("$_countdownValue", style: const TextStyle(color: AppColors.coral, fontSize: 160, fontWeight: FontWeight.w900)),
|
Text(
|
||||||
|
"$_countdownValue",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.coral,
|
||||||
|
fontSize: 160,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: _isCountingDown ? null : Padding(
|
floatingActionButton: _isCountingDown
|
||||||
padding: const EdgeInsets.only(bottom: 20.0),
|
? null
|
||||||
child: FloatingActionButton.extended(
|
: Padding(
|
||||||
onPressed: _isRunning ? _finishRun : _showStartConfirmation,
|
padding: const EdgeInsets.only(bottom: 20.0),
|
||||||
label: Text(
|
child: FloatingActionButton.extended(
|
||||||
_isRunning ? AppStrings.btnStop : AppStrings.btnStartRun,
|
onPressed: _isRunning ? _finishRun : _showStartConfirmation,
|
||||||
style: const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.5, fontSize: 16)
|
label: Text(
|
||||||
),
|
_isRunning ? AppStrings.btnStop : AppStrings.btnStartRun,
|
||||||
icon: Icon(_isRunning ? Icons.stop_rounded : Icons.play_arrow_rounded, size: 28),
|
style: const TextStyle(
|
||||||
backgroundColor: _isRunning ? AppColors.coral : Colors.white,
|
fontWeight: FontWeight.w900,
|
||||||
foregroundColor: _isRunning ? Colors.white : AppColors.background,
|
letterSpacing: 1.5,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
fontSize: 16,
|
||||||
elevation: 10,
|
),
|
||||||
),
|
),
|
||||||
),
|
icon: Icon(
|
||||||
|
_isRunning ? Icons.stop_rounded : Icons.play_arrow_rounded,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
backgroundColor: _isRunning ? AppColors.coral : Colors.white,
|
||||||
|
foregroundColor: _isRunning
|
||||||
|
? Colors.white
|
||||||
|
: AppColors.background,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
elevation: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -491,8 +728,22 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: const TextStyle(color: Colors.white54, fontWeight: FontWeight.bold, fontSize: 13)),
|
Text(
|
||||||
Text(value, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w900)),
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white54,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -508,22 +759,45 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
|||||||
return "${mins.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}";
|
return "${mins.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}";
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDivider() => Container(width: 1, height: 35, color: Colors.white10);
|
Widget _buildDivider() =>
|
||||||
|
Container(width: 1, height: 35, color: Colors.white10);
|
||||||
|
|
||||||
Widget _buildStat(String label, String value, String unit) {
|
Widget _buildStat(String label, String value, String unit) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: const TextStyle(color: Colors.white54, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1.5)),
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white54,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
letterSpacing: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
textBaseline: TextBaseline.alphabetic,
|
textBaseline: TextBaseline.alphabetic,
|
||||||
children: [
|
children: [
|
||||||
Text(value, style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w900)),
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (unit.isNotEmpty) ...[
|
if (unit.isNotEmpty) ...[
|
||||||
const SizedBox(width: 3),
|
const SizedBox(width: 3),
|
||||||
Text(unit, style: const TextStyle(color: Colors.white38, fontSize: 10, fontWeight: FontWeight.bold))
|
Text(
|
||||||
|
unit,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white38,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
// Estado dinâmico do utilizador
|
// Estado dinâmico do utilizador
|
||||||
double _dailyGoal = 0.0;
|
double _dailyGoal = 0.0;
|
||||||
double _currentDistance = 0.0;
|
double _currentDistance = 0.0;
|
||||||
double _bestDistance = 12.4;
|
double _bestDistance = 0.0; // Será atualizado do banco
|
||||||
double _bestSpeed = 16.8;
|
double _bestSpeed = 0.0; // Será atualizado do banco
|
||||||
int _steps = 0;
|
int _steps = 0;
|
||||||
int _totalTimeMinutes = 0;
|
int _totalTimeMinutes = 0;
|
||||||
|
|
||||||
double get _progress => _dailyGoal > 0 ? (_currentDistance / _dailyGoal).clamp(0.0, 1.0) : 0.0;
|
double get _progress =>
|
||||||
|
_dailyGoal > 0 ? (_currentDistance / _dailyGoal).clamp(0.0, 1.0) : 0.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -32,34 +33,107 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
_loadUserData();
|
_loadUserData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
// Recarregar dados quando a tela ganha foco (após retornar de uma corrida)
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_loadUserData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadUserData() async {
|
Future<void> _loadUserData() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final lastGoalDate = prefs.getString('last_goal_date');
|
final lastGoalDate = prefs.getString('last_goal_date');
|
||||||
final today = DateTime.now().toIso8601String().split('T')[0];
|
final today = DateTime.now().toIso8601String().split('T')[0];
|
||||||
|
|
||||||
setState(() {
|
// Buscar estatísticas do usuário do banco de dados
|
||||||
// Reset meta se o dia mudou
|
try {
|
||||||
if (lastGoalDate != today) {
|
final userStats = await SupabaseService.getUserStats();
|
||||||
_dailyGoal = 0.0;
|
|
||||||
prefs.remove('daily_goal');
|
// Buscar corridas de hoje para calcular distância total
|
||||||
} else {
|
await _loadTodayDistance();
|
||||||
_dailyGoal = prefs.getDouble('daily_goal') ?? 0.0;
|
|
||||||
|
setState(() {
|
||||||
|
// Reset meta se o dia mudou
|
||||||
|
if (lastGoalDate != today) {
|
||||||
|
_dailyGoal = 0.0;
|
||||||
|
prefs.remove('daily_goal');
|
||||||
|
} else {
|
||||||
|
_dailyGoal = prefs.getDouble('daily_goal') ?? 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualizar records do banco de dados
|
||||||
|
if (userStats != null) {
|
||||||
|
// Converter distância de metros para km (se necessário)
|
||||||
|
_bestDistance = (userStats['max_distance'] ?? 0.0) / 1000.0;
|
||||||
|
|
||||||
|
// Converter pace para velocidade (km/h)
|
||||||
|
// Pace é minutos por km, velocidade é km/h
|
||||||
|
// Velocidade = 60 / pace
|
||||||
|
final bestPace = userStats['best_pace'] ?? 0.0;
|
||||||
|
_bestSpeed = bestPace > 0 ? (60.0 / bestPace) : 0.0;
|
||||||
|
} else {
|
||||||
|
// Manter valores padrão se não houver dados
|
||||||
|
_bestDistance = 0.0;
|
||||||
|
_bestSpeed = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular passos com base na distância (7 passos a cada 5 metros)
|
||||||
|
_steps = ((_currentDistance * 1000) / 5 * 7).round();
|
||||||
|
_totalTimeMinutes = 0;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Em caso de erro, manter valores padrão
|
||||||
|
setState(() {
|
||||||
|
if (lastGoalDate != today) {
|
||||||
|
_dailyGoal = 0.0;
|
||||||
|
prefs.remove('daily_goal');
|
||||||
|
} else {
|
||||||
|
_dailyGoal = prefs.getDouble('daily_goal') ?? 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_bestDistance = 0.0;
|
||||||
|
_bestSpeed = 0.0;
|
||||||
|
_currentDistance = 0.0;
|
||||||
|
_steps = 0;
|
||||||
|
_totalTimeMinutes = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar corridas de hoje e calcular distância total
|
||||||
|
Future<void> _loadTodayDistance() async {
|
||||||
|
try {
|
||||||
|
final today = DateTime.now();
|
||||||
|
final startOfDay = DateTime(today.year, today.month, today.day);
|
||||||
|
final endOfDay = startOfDay.add(const Duration(days: 1));
|
||||||
|
|
||||||
|
final runs = await SupabaseService.getUserRuns(limit: 100);
|
||||||
|
|
||||||
|
double totalDistance = 0.0;
|
||||||
|
for (final run in runs) {
|
||||||
|
if (run['created_at'] != null) {
|
||||||
|
final runDate = DateTime.parse(run['created_at']);
|
||||||
|
if (runDate.isAfter(startOfDay) && runDate.isBefore(endOfDay)) {
|
||||||
|
totalDistance += (run['distance'] ?? 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No futuro, estes viriam do Supabase ou histórico local
|
_currentDistance = totalDistance / 1000.0; // Converter para km
|
||||||
|
} catch (e) {
|
||||||
_currentDistance = 0.0;
|
_currentDistance = 0.0;
|
||||||
_steps = 0;
|
}
|
||||||
_totalTimeMinutes = 0;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _saveGoal(double goal) async {
|
Future<void> _saveGoal(double goal) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final today = DateTime.now().toIso8601String().split('T')[0];
|
final today = DateTime.now().toIso8601String().split('T')[0];
|
||||||
|
|
||||||
await prefs.setDouble('daily_goal', goal);
|
await prefs.setDouble('daily_goal', goal);
|
||||||
await prefs.setString('last_goal_date', today);
|
await prefs.setString('last_goal_date', today);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_dailyGoal = goal;
|
_dailyGoal = goal;
|
||||||
});
|
});
|
||||||
@@ -71,21 +145,41 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: AppColors.backgroundGrey,
|
backgroundColor: AppColors.backgroundGrey,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
||||||
title: Text(AppStrings.defineDailyGoal, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
title: Text(
|
||||||
|
AppStrings.defineDailyGoal,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
...[5, 10, 15, 20].map((km) => ListTile(
|
...[5, 10, 15, 20].map(
|
||||||
title: Text("$km ${AppStrings.kmUnit}", style: const TextStyle(color: Colors.white)),
|
(km) => ListTile(
|
||||||
onTap: () {
|
title: Text(
|
||||||
_saveGoal(km.toDouble());
|
"$km ${AppStrings.kmUnit}",
|
||||||
Navigator.pop(context);
|
style: const TextStyle(color: Colors.white),
|
||||||
},
|
),
|
||||||
)),
|
onTap: () {
|
||||||
|
_saveGoal(km.toDouble());
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
const Divider(color: Colors.white10),
|
const Divider(color: Colors.white10),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.edit_note_rounded, color: AppColors.coral),
|
leading: const Icon(
|
||||||
title: Text(AppStrings.customGoal, style: const TextStyle(color: AppColors.coral, fontWeight: FontWeight.bold)),
|
Icons.edit_note_rounded,
|
||||||
|
color: AppColors.coral,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
AppStrings.customGoal,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.coral,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_showCustomGoalDialog();
|
_showCustomGoalDialog();
|
||||||
@@ -104,26 +198,41 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
backgroundColor: AppColors.backgroundGrey,
|
backgroundColor: AppColors.backgroundGrey,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
||||||
title: Text(AppStrings.customGoalTitle, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
title: Text(
|
||||||
|
AppStrings.customGoalTitle,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
content: TextField(
|
content: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d*'))],
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d*')),
|
||||||
|
],
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "Ex: 12.5",
|
hintText: "Ex: 12.5",
|
||||||
hintStyle: const TextStyle(color: Colors.white24),
|
hintStyle: const TextStyle(color: Colors.white24),
|
||||||
suffixText: AppStrings.kmUnit,
|
suffixText: AppStrings.kmUnit,
|
||||||
suffixStyle: const TextStyle(color: Colors.white54),
|
suffixStyle: const TextStyle(color: Colors.white54),
|
||||||
enabledBorder: const UnderlineInputBorder(borderSide: BorderSide(color: Colors.white24)),
|
enabledBorder: const UnderlineInputBorder(
|
||||||
focusedBorder: const UnderlineInputBorder(borderSide: BorderSide(color: AppColors.coral)),
|
borderSide: BorderSide(color: Colors.white24),
|
||||||
|
),
|
||||||
|
focusedBorder: const UnderlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: AppColors.coral),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: Text(AppStrings.btnCancel, style: const TextStyle(color: Colors.white54)),
|
child: Text(
|
||||||
|
AppStrings.btnCancel,
|
||||||
|
style: const TextStyle(color: Colors.white54),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -135,9 +244,17 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.coral,
|
backgroundColor: AppColors.coral,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
AppStrings.btnDefine,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(AppStrings.btnDefine, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -150,7 +267,10 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
valueListenable: AppStrings.languageNotifier,
|
valueListenable: AppStrings.languageNotifier,
|
||||||
builder: (context, language, child) {
|
builder: (context, language, child) {
|
||||||
final user = SupabaseService.currentUser;
|
final user = SupabaseService.currentUser;
|
||||||
final userName = user?.userMetadata?['name'] ?? user?.email?.split('@')[0] ?? AppStrings.userPlaceholder;
|
final userName =
|
||||||
|
user?.userMetadata?['name'] ??
|
||||||
|
user?.email?.split('@')[0] ??
|
||||||
|
AppStrings.userPlaceholder;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.background,
|
backgroundColor: AppColors.background,
|
||||||
@@ -167,12 +287,15 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -203,12 +326,24 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
children: [
|
children: [
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
Icons.bluetooth_audio_rounded,
|
Icons.bluetooth_audio_rounded,
|
||||||
() => Navigator.push(context, MaterialPageRoute(builder: (context) => const BluetoothConnectionScreen())),
|
() => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
const BluetoothConnectionScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
_buildIconButton(
|
_buildIconButton(
|
||||||
Icons.settings_rounded,
|
Icons.settings_rounded,
|
||||||
() => Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen())),
|
() => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
const SettingsScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -285,7 +420,7 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 30,
|
bottom: 30,
|
||||||
left: 50,
|
left: 50,
|
||||||
@@ -298,15 +433,22 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
color: AppColors.coral.withValues(alpha: 0.3),
|
color: AppColors.coral.withValues(alpha: 0.3),
|
||||||
blurRadius: 25,
|
blurRadius: 25,
|
||||||
spreadRadius: -5,
|
spreadRadius: -5,
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const GoogleMapScreen())),
|
onPressed: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const GoogleMapScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.coral,
|
backgroundColor: AppColors.coral,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(25),
|
||||||
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -316,7 +458,11 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
AppStrings.startTraining,
|
AppStrings.startTraining,
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, letterSpacing: 1.5),
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
letterSpacing: 1.5,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -353,7 +499,13 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
color: AppColors.backgroundGrey,
|
color: AppColors.backgroundGrey,
|
||||||
borderRadius: BorderRadius.circular(45),
|
borderRadius: BorderRadius.circular(45),
|
||||||
border: Border.all(color: Colors.white10, width: 2),
|
border: Border.all(color: Colors.white10, width: 2),
|
||||||
boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 20, offset: Offset(0, 10))],
|
boxShadow: const [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black26,
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -362,13 +514,20 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppStrings.dailyGoal,
|
AppStrings.dailyGoal,
|
||||||
style: const TextStyle(color: Colors.white54, fontWeight: FontWeight.bold, letterSpacing: 1),
|
style: const TextStyle(
|
||||||
|
color: Colors.white54,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (_dailyGoal > 0)
|
if (_dailyGoal > 0)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _showGoalDialog,
|
onTap: _showGoalDialog,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.coral.withValues(alpha: 0.1),
|
color: AppColors.coral.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
@@ -377,10 +536,17 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${(_progress * 100).toInt()}%",
|
"${(_progress * 100).toInt()}%",
|
||||||
style: const TextStyle(color: AppColors.coral, fontWeight: FontWeight.w900),
|
style: const TextStyle(
|
||||||
|
color: AppColors.coral,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
const Icon(Icons.edit_rounded, color: AppColors.coral, size: 14),
|
const Icon(
|
||||||
|
Icons.edit_rounded,
|
||||||
|
color: AppColors.coral,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -398,7 +564,9 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
value: _progress,
|
value: _progress,
|
||||||
strokeWidth: 15,
|
strokeWidth: 15,
|
||||||
backgroundColor: Colors.white.withValues(alpha: 0.05),
|
backgroundColor: Colors.white.withValues(alpha: 0.05),
|
||||||
valueColor: const AlwaysStoppedAnimation<Color>(AppColors.coral),
|
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||||
|
AppColors.coral,
|
||||||
|
),
|
||||||
strokeCap: StrokeCap.round,
|
strokeCap: StrokeCap.round,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -414,12 +582,21 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
color: AppColors.coral.withValues(alpha: 0.1),
|
color: AppColors.coral.withValues(alpha: 0.1),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.add_task_rounded, color: AppColors.coral, size: 40),
|
child: const Icon(
|
||||||
|
Icons.add_task_rounded,
|
||||||
|
color: AppColors.coral,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
AppStrings.setGoal,
|
AppStrings.setGoal,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w900, letterSpacing: 1),
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -428,14 +605,30 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(AppStrings.distance, style: const TextStyle(color: Colors.white38, fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: 2)),
|
Text(
|
||||||
|
AppStrings.distance,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white38,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
_currentDistance.toStringAsFixed(1),
|
_currentDistance.toStringAsFixed(1),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 48, fontWeight: FontWeight.w900),
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"/ ${_dailyGoal.toStringAsFixed(1)} ${AppStrings.kmUnit}",
|
"/ ${_dailyGoal.toStringAsFixed(1)} ${AppStrings.kmUnit}",
|
||||||
style: const TextStyle(color: Colors.white54, fontSize: 14, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
color: Colors.white54,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -445,11 +638,14 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_buildSimpleStat(AppStrings.steps, "${(_steps / 1000).toStringAsFixed(1)}k"),
|
_buildSimpleStat(
|
||||||
|
AppStrings.steps,
|
||||||
|
"${(_steps / 1000).toStringAsFixed(1)}k",
|
||||||
|
),
|
||||||
const SizedBox(width: 40),
|
const SizedBox(width: 40),
|
||||||
_buildSimpleStat(AppStrings.time, "${_totalTimeMinutes}m"),
|
_buildSimpleStat(AppStrings.time, "${_totalTimeMinutes}m"),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -458,13 +654,33 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
Widget _buildSimpleStat(String label, String value) {
|
Widget _buildSimpleStat(String label, String value) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text(value, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 16)),
|
Text(
|
||||||
Text(label, style: const TextStyle(color: Colors.white38, fontSize: 9, fontWeight: FontWeight.bold)),
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white38,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRecordCard(String title, String value, String unit, IconData icon, Color accentColor) {
|
Widget _buildRecordCard(
|
||||||
|
String title,
|
||||||
|
String value,
|
||||||
|
String unit,
|
||||||
|
IconData icon,
|
||||||
|
Color accentColor,
|
||||||
|
) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -477,7 +693,10 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(color: accentColor.withValues(alpha: 0.1), shape: BoxShape.circle),
|
decoration: BoxDecoration(
|
||||||
|
color: accentColor.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
child: Icon(icon, color: accentColor, size: 18),
|
child: Icon(icon, color: accentColor, size: 18),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
@@ -485,18 +704,46 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
textBaseline: TextBaseline.alphabetic,
|
textBaseline: TextBaseline.alphabetic,
|
||||||
children: [
|
children: [
|
||||||
Text(value, style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w900)),
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(unit, style: const TextStyle(color: Colors.white38, fontSize: 10, fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
unit,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white38,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(title, style: const TextStyle(color: Colors.white54, fontSize: 9, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white54,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildWideRecordCard(String title, String value, String unit, IconData icon, Color accentColor) {
|
Widget _buildWideRecordCard(
|
||||||
|
String title,
|
||||||
|
String value,
|
||||||
|
String unit,
|
||||||
|
IconData icon,
|
||||||
|
Color accentColor,
|
||||||
|
) {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
@@ -509,7 +756,10 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(color: accentColor.withValues(alpha: 0.1), shape: BoxShape.circle),
|
decoration: BoxDecoration(
|
||||||
|
color: accentColor.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
child: Icon(icon, color: accentColor, size: 24),
|
child: Icon(icon, color: accentColor, size: 24),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
@@ -520,12 +770,34 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
textBaseline: TextBaseline.alphabetic,
|
textBaseline: TextBaseline.alphabetic,
|
||||||
children: [
|
children: [
|
||||||
Text(value, style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w900)),
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(unit, style: const TextStyle(color: Colors.white38, fontSize: 12, fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
unit,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white38,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(title, style: const TextStyle(color: Colors.white54, fontSize: 11, fontWeight: FontWeight.bold, letterSpacing: 0.5)),
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white54,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class SupabaseService {
|
|||||||
static Future<void> initialize() async {
|
static Future<void> initialize() async {
|
||||||
try {
|
try {
|
||||||
print('DEBUG: Inicializando Supabase...');
|
print('DEBUG: Inicializando Supabase...');
|
||||||
|
|
||||||
await Supabase.initialize(
|
await Supabase.initialize(
|
||||||
url: AppConstants.supabaseUrl,
|
url: AppConstants.supabaseUrl,
|
||||||
anonKey: AppConstants.supabaseAnonKey,
|
anonKey: AppConstants.supabaseAnonKey,
|
||||||
@@ -110,4 +110,118 @@ class SupabaseService {
|
|||||||
// Listen to auth state changes
|
// Listen to auth state changes
|
||||||
static Stream<AuthState> get authStateChanges =>
|
static Stream<AuthState> get authStateChanges =>
|
||||||
_supabase.auth.onAuthStateChange;
|
_supabase.auth.onAuthStateChange;
|
||||||
|
|
||||||
|
// Save a new run
|
||||||
|
static Future<void> saveRun({
|
||||||
|
required double distance,
|
||||||
|
required double pace,
|
||||||
|
required int duration,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final userId = currentUser?.id;
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception('Usuário não autenticado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new run
|
||||||
|
await _supabase.from('runs').insert({
|
||||||
|
'id_user': userId,
|
||||||
|
'distance': distance,
|
||||||
|
'pace': pace,
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update user stats
|
||||||
|
await _updateUserStats(userId, distance, pace);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Erro ao salvar corrida: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user statistics
|
||||||
|
static Future<void> _updateUserStats(
|
||||||
|
String userId,
|
||||||
|
double newDistance,
|
||||||
|
double newPace,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
// Get current user stats
|
||||||
|
final currentStats = await _supabase
|
||||||
|
.from('user_stats')
|
||||||
|
.select('best_pace, max_distance')
|
||||||
|
.eq('id_user', userId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (currentStats == null) {
|
||||||
|
// Create new user stats record
|
||||||
|
await _supabase.from('user_stats').insert({
|
||||||
|
'id_user': userId,
|
||||||
|
'best_pace': newPace,
|
||||||
|
'max_distance': newDistance,
|
||||||
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Update if new records are better
|
||||||
|
final updates = <String, dynamic>{};
|
||||||
|
|
||||||
|
if (currentStats['max_distance'] == null ||
|
||||||
|
newDistance > currentStats['max_distance']) {
|
||||||
|
updates['max_distance'] = newDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStats['best_pace'] == null ||
|
||||||
|
newPace < currentStats['best_pace']) {
|
||||||
|
updates['best_pace'] = newPace;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.isNotEmpty) {
|
||||||
|
await _supabase
|
||||||
|
.from('user_stats')
|
||||||
|
.update(updates)
|
||||||
|
.eq('id_user', userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Erro ao atualizar estatísticas: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user statistics
|
||||||
|
static Future<Map<String, dynamic>?> getUserStats() async {
|
||||||
|
try {
|
||||||
|
final userId = currentUser?.id;
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception('Usuário não autenticado');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _supabase
|
||||||
|
.from('user_stats')
|
||||||
|
.select('*')
|
||||||
|
.eq('id_user', userId)
|
||||||
|
.maybeSingle();
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Erro ao buscar estatísticas: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user runs history
|
||||||
|
static Future<List<Map<String, dynamic>>> getUserRuns({
|
||||||
|
int limit = 50,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final userId = currentUser?.id;
|
||||||
|
if (userId == null) {
|
||||||
|
throw Exception('Usuário não autenticado');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _supabase
|
||||||
|
.from('runs')
|
||||||
|
.select('*')
|
||||||
|
.eq('id_user', userId)
|
||||||
|
.order('created_at', ascending: false)
|
||||||
|
.limit(limit);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Erro ao buscar histórico de corridas: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
pubspec.lock
18
pubspec.lock
@@ -85,10 +85,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.0"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -548,18 +548,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.19"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -809,7 +809,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||||
@@ -945,10 +945,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.7"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ dependencies:
|
|||||||
lottie: ^3.1.2
|
lottie: ^3.1.2
|
||||||
supabase_flutter: ^2.6.0
|
supabase_flutter: ^2.6.0
|
||||||
flutter_polyline_points: ^2.1.0
|
flutter_polyline_points: ^2.1.0
|
||||||
|
shared_preferences: ^2.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user