diff --git a/lib/screens/google_map_screen.dart b/lib/screens/google_map_screen.dart index c2ab2c0..55b51da 100644 --- a/lib/screens/google_map_screen.dart +++ b/lib/screens/google_map_screen.dart @@ -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 { await _initTracking(); } - Future _createArrowMarker(Color color, Color borderColor, double size) async { + Future _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 { 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 { 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 { } 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 { 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 { 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 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 { 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 { _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 { 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 { 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 { 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 { ), ), 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 { 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 { } 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 { 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 _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 { 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 { 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 { 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, + ), + ), ], ], ), diff --git a/lib/screens/logado_screen.dart b/lib/screens/logado_screen.dart index abd4dc9..ba3246c 100644 --- a/lib/screens/logado_screen.dart +++ b/lib/screens/logado_screen.dart @@ -19,12 +19,13 @@ class _LogadoScreenState extends State { // 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 { _loadUserData(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Recarregar dados quando a tela ganha foco (após retornar de uma corrida) + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadUserData(); + }); + } + Future _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 _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 _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 { 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 { 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 { }, 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 { 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 { ), ), ), - + 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 { 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 { ], ), ), - + Positioned( bottom: 30, left: 50, @@ -298,15 +433,22 @@ class _LogadoScreenState extends State { 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 { 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 { 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 { 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 { 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 { value: _progress, strokeWidth: 15, backgroundColor: Colors.white.withValues(alpha: 0.05), - valueColor: const AlwaysStoppedAnimation(AppColors.coral), + valueColor: const AlwaysStoppedAnimation( + AppColors.coral, + ), strokeCap: StrokeCap.round, ), ), @@ -414,12 +582,21 @@ class _LogadoScreenState extends State { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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, + ), + ), ], ), ], diff --git a/lib/services/supabase_service.dart b/lib/services/supabase_service.dart index 941c6cf..eb7768d 100644 --- a/lib/services/supabase_service.dart +++ b/lib/services/supabase_service.dart @@ -8,7 +8,7 @@ class SupabaseService { static Future 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 get authStateChanges => _supabase.auth.onAuthStateChange; + + // Save a new run + static Future 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 _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 = {}; + + 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?> 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>> 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'); + } + } } diff --git a/pubspec.lock b/pubspec.lock index 889f011..1159b0c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" clock: dependency: transitive description: @@ -548,18 +548,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -809,7 +809,7 @@ packages: source: hosted version: "2.1.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" @@ -945,10 +945,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.7" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6179f80..e2d1207 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: lottie: ^3.1.2 supabase_flutter: ^2.6.0 flutter_polyline_points: ^2.1.0 + shared_preferences: ^2.3.2 dev_dependencies: flutter_test: