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 '../constants/app_colors.dart';
|
||||
import '../constants/app_strings.dart';
|
||||
import '../services/supabase_service.dart';
|
||||
|
||||
class GoogleMapScreen extends StatefulWidget {
|
||||
const GoogleMapScreen({super.key});
|
||||
@@ -49,7 +50,11 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
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 Canvas canvas = Canvas(recorder);
|
||||
final Path path = Path();
|
||||
@@ -60,17 +65,29 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
path.close();
|
||||
|
||||
canvas.drawPath(
|
||||
path.shift(const Offset(0, 2)),
|
||||
Paint()
|
||||
..color = Colors.black38
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2));
|
||||
path.shift(const Offset(0, 2)),
|
||||
Paint()
|
||||
..color = Colors.black38
|
||||
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2),
|
||||
);
|
||||
canvas.drawPath(path, Paint()..color = color);
|
||||
|
||||
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 ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
final ui.Image image = await recorder.endRecording().toImage(
|
||||
size.toInt(),
|
||||
size.toInt() + 4,
|
||||
);
|
||||
final ByteData? byteData = await image.toByteData(
|
||||
format: ui.ImageByteFormat.png,
|
||||
);
|
||||
return BitmapDescriptor.bytes(byteData!.buffer.asUint8List());
|
||||
}
|
||||
|
||||
@@ -90,7 +107,9 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
permission = await Geolocator.requestPermission();
|
||||
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);
|
||||
_routePoints.add(_currentPosition);
|
||||
|
||||
@@ -100,15 +119,23 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
}
|
||||
|
||||
void _startLocationStream() {
|
||||
_positionStreamSubscription = Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 1)
|
||||
).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);
|
||||
}
|
||||
});
|
||||
_positionStreamSubscription =
|
||||
Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.bestForNavigation,
|
||||
distanceFilter: 1,
|
||||
),
|
||||
).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) {
|
||||
@@ -116,19 +143,21 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
setState(() {
|
||||
double currentSpeedKmh = speed * 3.6;
|
||||
if (currentSpeedKmh < 0) currentSpeedKmh = 0;
|
||||
|
||||
|
||||
_currentSpeed = currentSpeedKmh;
|
||||
|
||||
if (_isRunning) {
|
||||
if (currentSpeedKmh > _maxSpeed) {
|
||||
_maxSpeed = currentSpeedKmh;
|
||||
}
|
||||
|
||||
|
||||
_totalDistance += Geolocator.distanceBetween(
|
||||
_currentPosition.latitude, _currentPosition.longitude,
|
||||
newPoint.latitude, newPoint.longitude
|
||||
_currentPosition.latitude,
|
||||
_currentPosition.longitude,
|
||||
newPoint.latitude,
|
||||
newPoint.longitude,
|
||||
);
|
||||
|
||||
|
||||
_routePoints.add(newPoint);
|
||||
_updateTraveledPolylines();
|
||||
} else {
|
||||
@@ -148,42 +177,53 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
void _updateTraveledPolylines() {
|
||||
if (_routePoints.length > 1) {
|
||||
_polylines.clear();
|
||||
_polylines.add(Polyline(
|
||||
polylineId: const PolylineId('route_glow'),
|
||||
points: List.from(_routePoints),
|
||||
color: AppColors.coral.withOpacity(0.3),
|
||||
width: 12,
|
||||
zIndex: 9
|
||||
));
|
||||
_polylines.add(Polyline(
|
||||
polylineId: const PolylineId('route'),
|
||||
points: List.from(_routePoints),
|
||||
color: AppColors.coral,
|
||||
width: 5,
|
||||
jointType: JointType.round,
|
||||
zIndex: 10
|
||||
));
|
||||
_polylines.add(
|
||||
Polyline(
|
||||
polylineId: const PolylineId('route_glow'),
|
||||
points: List.from(_routePoints),
|
||||
color: AppColors.coral.withOpacity(0.3),
|
||||
width: 12,
|
||||
zIndex: 9,
|
||||
),
|
||||
);
|
||||
_polylines.add(
|
||||
Polyline(
|
||||
polylineId: const PolylineId('route'),
|
||||
points: List.from(_routePoints),
|
||||
color: AppColors.coral,
|
||||
width: 5,
|
||||
jointType: JointType.round,
|
||||
zIndex: 10,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateMarkers() {
|
||||
_markers.clear();
|
||||
_markers.add(Marker(
|
||||
_markers.add(
|
||||
Marker(
|
||||
markerId: const MarkerId('follower'),
|
||||
position: _currentPosition,
|
||||
rotation: _calculateRotation(_routePoints),
|
||||
flat: true,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
icon: _arrowIcon ?? BitmapDescriptor.defaultMarker,
|
||||
zIndex: 12
|
||||
));
|
||||
zIndex: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _calculateRotation(List<LatLng> points) {
|
||||
if (points.length < 2) return 0;
|
||||
LatLng p1 = points[points.length - 2];
|
||||
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() {
|
||||
@@ -192,17 +232,43 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppColors.backgroundGrey,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
|
||||
title: Text(AppStrings.startRunQuestion, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
content: Text(AppStrings.startRunDescription, style: const TextStyle(color: Colors.white70)),
|
||||
title: Text(
|
||||
AppStrings.startRunQuestion,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
AppStrings.startRunDescription,
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
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(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_startCountdown();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.coral, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
|
||||
child: Text(AppStrings.yes, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
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;
|
||||
});
|
||||
|
||||
// Calculate pace (minutes per kilometer)
|
||||
final pace = _calculatePace(finalDistance, finalTime);
|
||||
|
||||
// Save run data to database
|
||||
_saveRunData(finalDistance, pace, finalTime);
|
||||
|
||||
showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
@@ -266,7 +338,10 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
child: AlertDialog(
|
||||
backgroundColor: AppColors.background,
|
||||
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(
|
||||
width: MediaQuery.of(context).size.width * 0.85,
|
||||
padding: const EdgeInsets.all(25),
|
||||
@@ -275,11 +350,26 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
decoration: const BoxDecoration(color: AppColors.coral, shape: BoxShape.circle),
|
||||
child: const Icon(Icons.emoji_events_rounded, color: Colors.white, size: 40),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.coral,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.emoji_events_rounded,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
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),
|
||||
Container(
|
||||
height: 180,
|
||||
@@ -291,18 +381,34 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(23),
|
||||
child: GoogleMap(
|
||||
initialCameraPosition: CameraPosition(target: finalRoute.isNotEmpty ? finalRoute.first : _currentPosition, zoom: 15),
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: finalRoute.isNotEmpty
|
||||
? finalRoute.first
|
||||
: _currentPosition,
|
||||
zoom: 15,
|
||||
),
|
||||
onMapCreated: (controller) {
|
||||
if (finalRoute.isNotEmpty) {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
controller.animateCamera(CameraUpdate.newLatLngBounds(
|
||||
_getBounds(finalRoute), 40
|
||||
));
|
||||
});
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() {
|
||||
controller.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
_getBounds(finalRoute),
|
||||
40,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
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,
|
||||
myLocationButtonEnabled: false,
|
||||
@@ -313,11 +419,20 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
_buildResultRow(AppStrings.totalDistance, _formatDistance(finalDistance)),
|
||||
_buildResultRow(
|
||||
AppStrings.totalDistance,
|
||||
_formatDistance(finalDistance),
|
||||
),
|
||||
const Divider(color: Colors.white10, height: 25),
|
||||
_buildResultRow(AppStrings.totalTime, _formatTime(finalTime)),
|
||||
_buildResultRow(
|
||||
AppStrings.totalTime,
|
||||
_formatTime(finalTime),
|
||||
),
|
||||
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),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -334,10 +449,18 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: AppColors.background,
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
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) {
|
||||
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}";
|
||||
}
|
||||
|
||||
@@ -379,72 +503,157 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_isRunning ? AppStrings.mapTitleRunning : AppStrings.appTitle.toUpperCase(),
|
||||
style: const TextStyle(fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 2, fontSize: 18)
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
_isRunning
|
||||
? AppStrings.mapTitleRunning
|
||||
: AppStrings.appTitle.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
letterSpacing: 2,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context)
|
||||
)
|
||||
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.94,
|
||||
height: MediaQuery.of(context).size.height * 0.72,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.backgroundGrey,
|
||||
borderRadius: BorderRadius.circular(55),
|
||||
border: Border.all(color: Colors.white10, width: 3),
|
||||
boxShadow: const [BoxShadow(color: Colors.black54, blurRadius: 40, spreadRadius: -10)]
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(52),
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator(color: AppColors.coral))
|
||||
: GoogleMap(
|
||||
initialCameraPosition: CameraPosition(target: _currentPosition, zoom: 17.5),
|
||||
onMapCreated: (controller) => _mapController = controller,
|
||||
markers: _markers,
|
||||
polylines: _polylines,
|
||||
zoomControlsEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
compassEnabled: false,
|
||||
mapToolbarEnabled: false,
|
||||
width: MediaQuery.of(context).size.width * 0.94,
|
||||
height: MediaQuery.of(context).size.height * 0.72,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.backgroundGrey,
|
||||
borderRadius: BorderRadius.circular(55),
|
||||
border: Border.all(color: Colors.white10, width: 3),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black54,
|
||||
blurRadius: 40,
|
||||
spreadRadius: -10,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(52),
|
||||
child: _isLoading
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.coral,
|
||||
),
|
||||
)
|
||||
)
|
||||
: 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(
|
||||
top: 115, left: 25, right: 25,
|
||||
top: 115,
|
||||
left: 25,
|
||||
right: 25,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 22, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.background.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: AppColors.background.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildStat(AppStrings.mapPace, _currentSpeed.toStringAsFixed(1), AppStrings.kmhUnit),
|
||||
_buildStat(
|
||||
AppStrings.mapPace,
|
||||
_currentSpeed.toStringAsFixed(1),
|
||||
AppStrings.kmhUnit,
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildStat(AppStrings.mapRoute, _formatDistanceValue(_totalDistance), _totalDistance < 1000 ? AppStrings.metersUnit : AppStrings.kmUnit),
|
||||
_buildStat(
|
||||
AppStrings.mapRoute,
|
||||
_formatDistanceValue(_totalDistance),
|
||||
_totalDistance < 1000
|
||||
? AppStrings.metersUnit
|
||||
: AppStrings.kmUnit,
|
||||
),
|
||||
_buildDivider(),
|
||||
_buildStat(AppStrings.mapTime, _formatTimeShort(_secondsElapsed), ""),
|
||||
_buildStat(
|
||||
AppStrings.mapTime,
|
||||
_formatTimeShort(_secondsElapsed),
|
||||
"",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -459,30 +668,58 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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),
|
||||
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(
|
||||
padding: const EdgeInsets.only(bottom: 20.0),
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: _isRunning ? _finishRun : _showStartConfirmation,
|
||||
label: Text(
|
||||
_isRunning ? AppStrings.btnStop : AppStrings.btnStartRun,
|
||||
style: const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.5, fontSize: 16)
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
floatingActionButton: _isCountingDown
|
||||
? null
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20.0),
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: _isRunning ? _finishRun : _showStartConfirmation,
|
||||
label: Text(
|
||||
_isRunning ? AppStrings.btnStop : AppStrings.btnStartRun,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 1.5,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -491,8 +728,22 @@ class _GoogleMapScreenState extends State<GoogleMapScreen> {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(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)),
|
||||
Text(
|
||||
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')}";
|
||||
}
|
||||
|
||||
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) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
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) ...[
|
||||
const SizedBox(width: 3),
|
||||
Text(unit, style: const TextStyle(color: Colors.white38, fontSize: 10, fontWeight: FontWeight.bold))
|
||||
const SizedBox(width: 3),
|
||||
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
|
||||
double _dailyGoal = 0.0;
|
||||
double _currentDistance = 0.0;
|
||||
double _bestDistance = 12.4;
|
||||
double _bestSpeed = 16.8;
|
||||
double _bestDistance = 0.0; // Será atualizado do banco
|
||||
double _bestSpeed = 0.0; // Será atualizado do banco
|
||||
int _steps = 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
|
||||
void initState() {
|
||||
@@ -32,34 +33,107 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
_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 {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final lastGoalDate = prefs.getString('last_goal_date');
|
||||
final today = DateTime.now().toIso8601String().split('T')[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;
|
||||
// Buscar estatísticas do usuário do banco de dados
|
||||
try {
|
||||
final userStats = await SupabaseService.getUserStats();
|
||||
|
||||
// Buscar corridas de hoje para calcular distância total
|
||||
await _loadTodayDistance();
|
||||
|
||||
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;
|
||||
_steps = 0;
|
||||
_totalTimeMinutes = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveGoal(double goal) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final today = DateTime.now().toIso8601String().split('T')[0];
|
||||
|
||||
|
||||
await prefs.setDouble('daily_goal', goal);
|
||||
await prefs.setString('last_goal_date', today);
|
||||
|
||||
|
||||
setState(() {
|
||||
_dailyGoal = goal;
|
||||
});
|
||||
@@ -71,21 +145,41 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppColors.backgroundGrey,
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...[5, 10, 15, 20].map((km) => ListTile(
|
||||
title: Text("$km ${AppStrings.kmUnit}", style: const TextStyle(color: Colors.white)),
|
||||
onTap: () {
|
||||
_saveGoal(km.toDouble());
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
...[5, 10, 15, 20].map(
|
||||
(km) => ListTile(
|
||||
title: Text(
|
||||
"$km ${AppStrings.kmUnit}",
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
onTap: () {
|
||||
_saveGoal(km.toDouble());
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(color: Colors.white10),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit_note_rounded, color: AppColors.coral),
|
||||
title: Text(AppStrings.customGoal, style: const TextStyle(color: AppColors.coral, fontWeight: FontWeight.bold)),
|
||||
leading: const Icon(
|
||||
Icons.edit_note_rounded,
|
||||
color: AppColors.coral,
|
||||
),
|
||||
title: Text(
|
||||
AppStrings.customGoal,
|
||||
style: const TextStyle(
|
||||
color: AppColors.coral,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_showCustomGoalDialog();
|
||||
@@ -104,26 +198,41 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppColors.backgroundGrey,
|
||||
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(
|
||||
controller: controller,
|
||||
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),
|
||||
decoration: InputDecoration(
|
||||
hintText: "Ex: 12.5",
|
||||
hintStyle: const TextStyle(color: Colors.white24),
|
||||
suffixText: AppStrings.kmUnit,
|
||||
suffixStyle: const TextStyle(color: Colors.white54),
|
||||
enabledBorder: const UnderlineInputBorder(borderSide: BorderSide(color: Colors.white24)),
|
||||
focusedBorder: const UnderlineInputBorder(borderSide: BorderSide(color: AppColors.coral)),
|
||||
enabledBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.white24),
|
||||
),
|
||||
focusedBorder: const UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: AppColors.coral),
|
||||
),
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
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(
|
||||
onPressed: () {
|
||||
@@ -135,9 +244,17 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
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,
|
||||
builder: (context, language, child) {
|
||||
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(
|
||||
backgroundColor: AppColors.background,
|
||||
@@ -167,12 +287,15 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -203,12 +326,24 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
children: [
|
||||
_buildIconButton(
|
||||
Icons.bluetooth_audio_rounded,
|
||||
() => Navigator.push(context, MaterialPageRoute(builder: (context) => const BluetoothConnectionScreen())),
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const BluetoothConnectionScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildIconButton(
|
||||
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(
|
||||
bottom: 30,
|
||||
left: 50,
|
||||
@@ -298,15 +433,22 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
color: AppColors.coral.withValues(alpha: 0.3),
|
||||
blurRadius: 25,
|
||||
spreadRadius: -5,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => const GoogleMapScreen())),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const GoogleMapScreen(),
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.coral,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: Row(
|
||||
@@ -316,7 +458,11 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
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,
|
||||
borderRadius: BorderRadius.circular(45),
|
||||
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(
|
||||
children: [
|
||||
@@ -362,13 +514,20 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
children: [
|
||||
Text(
|
||||
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)
|
||||
GestureDetector(
|
||||
onTap: _showGoalDialog,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.coral.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
@@ -377,10 +536,17 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
children: [
|
||||
Text(
|
||||
"${(_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 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,
|
||||
strokeWidth: 15,
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.05),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(AppColors.coral),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
AppColors.coral,
|
||||
),
|
||||
strokeCap: StrokeCap.round,
|
||||
),
|
||||
),
|
||||
@@ -414,12 +582,21 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
color: AppColors.coral.withValues(alpha: 0.1),
|
||||
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),
|
||||
Text(
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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(
|
||||
_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(
|
||||
"/ ${_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(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildSimpleStat(AppStrings.steps, "${(_steps / 1000).toStringAsFixed(1)}k"),
|
||||
_buildSimpleStat(
|
||||
AppStrings.steps,
|
||||
"${(_steps / 1000).toStringAsFixed(1)}k",
|
||||
),
|
||||
const SizedBox(width: 40),
|
||||
_buildSimpleStat(AppStrings.time, "${_totalTimeMinutes}m"),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -458,13 +654,33 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
Widget _buildSimpleStat(String label, String value) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(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)),
|
||||
Text(
|
||||
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(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -477,7 +693,10 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
children: [
|
||||
Container(
|
||||
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),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
@@ -485,18 +704,46 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
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),
|
||||
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(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
@@ -509,7 +756,10 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
children: [
|
||||
Container(
|
||||
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),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
@@ -520,12 +770,34 @@ class _LogadoScreenState extends State<LogadoScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
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),
|
||||
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 {
|
||||
try {
|
||||
print('DEBUG: Inicializando Supabase...');
|
||||
|
||||
|
||||
await Supabase.initialize(
|
||||
url: AppConstants.supabaseUrl,
|
||||
anonKey: AppConstants.supabaseAnonKey,
|
||||
@@ -110,4 +110,118 @@ class SupabaseService {
|
||||
// Listen to auth state changes
|
||||
static Stream<AuthState> get authStateChanges =>
|
||||
_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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user