diff --git a/lib/constants/app_strings.dart b/lib/constants/app_strings.dart index dde70c8..cf77044 100644 --- a/lib/constants/app_strings.dart +++ b/lib/constants/app_strings.dart @@ -44,8 +44,10 @@ class AppStrings { static const String mapTitleRunning = "CORRIDA"; static const String mapPace = "RITMO"; static const String mapRoute = "TRAJETO"; + static const String mapTime = "TEMPO"; static const String kmhUnit = "KM/H"; static const String kmUnit = "KM"; + static const String metersUnit = "m"; static const String planningInstruction = "Toque para definir Início e Fim"; static const String btnStop = "PARAR"; static const String btnSimulate = "SIMULAR"; @@ -53,6 +55,15 @@ class AppStrings { static const String btnStopRun = "PARAR CORRIDA"; static const String startPoint = "Partida"; static const String finishPoint = "Chegada"; + static const String markDestination = "Marcar Destino"; + static const String chooseRoute = "Escolher Rota"; + static const String confirmDestination = "Confirmar Destino?"; + static const String cancel = "Cancelar"; + static const String yes = "Sim"; + static const String runFinished = "Corrida Finalizada!"; + static const String totalDistance = "Distância Total"; + static const String totalTime = "Tempo Total"; + static const String close = "Fechar"; // Auth Screens static const String entrar = "ENTRAR"; diff --git a/lib/screens/google_map_screen.dart b/lib/screens/google_map_screen.dart index f2bb209..e0d337f 100644 --- a/lib/screens/google_map_screen.dart +++ b/lib/screens/google_map_screen.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:geolocator/geolocator.dart'; +import 'package:flutter_polyline_points/flutter_polyline_points.dart'; import '../constants/app_colors.dart'; import '../constants/app_strings.dart'; @@ -16,29 +16,32 @@ class GoogleMapScreen extends StatefulWidget { } class _GoogleMapScreenState extends State { + // CONFIGURAÇÃO: Insira aqui sua chave da Google Cloud com Directions API ativa + final String googleApiKey = "AIzaSyCk84rxmF044cxKLABf55rEKHDqOcyoV5k"; + GoogleMapController? _mapController; StreamSubscription? _positionStreamSubscription; - Timer? _simulationTimer; + Timer? _stopwatchTimer; - // Controle de frequência de atualização para evitar sobrecarga e crashes DateTime? _lastUpdate; - final List _routePoints = []; + List _remainingPlannedPoints = []; // Lista para o trajeto que encurta final Set _polylines = {}; final Set _markers = {}; - LatLng? _plannedStart; LatLng? _plannedEnd; bool _isPlanningMode = false; bool _isRunning = false; + bool _isLoading = true; double _currentSpeed = 0.0; double _totalDistance = 0.0; LatLng _currentPosition = const LatLng(38.7223, -9.1393); - bool _isLoading = true; - bool _isSimulating = false; - BitmapDescriptor? _startIcon; + int _secondsElapsed = 0; + int _countdownValue = 3; + bool _isCountingDown = false; + BitmapDescriptor? _arrowIcon; BitmapDescriptor? _finishIcon; @@ -49,27 +52,34 @@ class _GoogleMapScreenState extends State { } Future _setupIconsAndTracking() async { - // Marcadores premium: tamanho ideal para visibilidade e estética - _startIcon = await _createPremiumMarker(Colors.greenAccent, Icons.play_arrow_rounded, 85); - _finishIcon = await _createPremiumMarker(AppColors.coral, Icons.flag_rounded, 85); - _arrowIcon = await _createArrowMarker(Colors.black, Colors.white, 95); + _finishIcon = await _createPremiumMarker(AppColors.coral, Icons.flag_rounded, 65); + // Borda agora é proporcional ao tamanho (25). + _arrowIcon = await _createArrowMarker(Colors.black, Colors.white, 85); await _initTracking(); } Future _createPremiumMarker(Color color, IconData icon, double size) async { final ui.PictureRecorder recorder = ui.PictureRecorder(); final Canvas canvas = Canvas(recorder); - final Paint shadowPaint = Paint()..color = Colors.black.withOpacity(0.4)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6); + final Paint shadowPaint = Paint() + ..color = Colors.black.withOpacity(0.4) + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6); canvas.drawCircle(Offset(size / 2, size / 2 + 3), size / 2, shadowPaint); canvas.drawCircle(Offset(size / 2, size / 2), size / 2, Paint()..color = Colors.white); canvas.drawCircle(Offset(size / 2, size / 2), size / 2 - 5, Paint()..color = color); TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr); - textPainter.text = TextSpan(text: String.fromCharCode(icon.codePoint), style: TextStyle(fontSize: size * 0.6, fontFamily: icon.fontFamily, color: Colors.white, fontWeight: FontWeight.bold)); + textPainter.text = TextSpan( + text: String.fromCharCode(icon.codePoint), + style: TextStyle( + fontSize: size * 0.6, + fontFamily: icon.fontFamily, + color: Colors.white, + fontWeight: FontWeight.bold)); textPainter.layout(); textPainter.paint(canvas, Offset((size - textPainter.width) / 2, (size - textPainter.height) / 2)); final ui.Image image = await recorder.endRecording().toImage(size.toInt(), size.toInt() + 6); final ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png); - return BitmapDescriptor.fromBytes(byteData!.buffer.asUint8List()); + return BitmapDescriptor.bytes(byteData!.buffer.asUint8List()); } Future _createArrowMarker(Color color, Color borderColor, double size) async { @@ -81,18 +91,27 @@ class _GoogleMapScreenState extends State { path.lineTo(size / 2, size * 0.7); path.lineTo(size * 0.1, size); path.close(); - canvas.drawPath(path.shift(const Offset(0, 4)), Paint()..color = Colors.black38..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4)); + + canvas.drawPath( + path.shift(const Offset(0, 4)), + Paint() + ..color = Colors.black38 + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4)); canvas.drawPath(path, Paint()..color = color); - canvas.drawPath(path, Paint()..color = borderColor..style = ui.PaintingStyle.stroke..strokeWidth = 7); + + // CORREÇÃO: Borda escala proporcionalmente (12% do tamanho) + double strokeWidth = size * 0.12; + canvas.drawPath(path, Paint()..color = borderColor..style = ui.PaintingStyle.stroke..strokeWidth = strokeWidth); + final ui.Image image = await recorder.endRecording().toImage(size.toInt(), size.toInt() + 6); final ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png); - return BitmapDescriptor.fromBytes(byteData!.buffer.asUint8List()); + return BitmapDescriptor.bytes(byteData!.buffer.asUint8List()); } @override void dispose() { _positionStreamSubscription?.cancel(); - _simulationTimer?.cancel(); + _stopwatchTimer?.cancel(); _mapController?.dispose(); super.dispose(); } @@ -107,58 +126,68 @@ class _GoogleMapScreenState extends State { } Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.bestForNavigation); _currentPosition = LatLng(position.latitude, position.longitude); + + // Adiciona ponto inicial para permitir rotação imediata + _routePoints.add(_currentPosition); + setState(() => _isLoading = false); + _updateMarkers(); // Força a exibição imediata _startLocationStream(); } void _startLocationStream() { _positionStreamSubscription = Geolocator.getPositionStream( - locationSettings: const LocationSettings( - accuracy: LocationAccuracy.bestForNavigation, - distanceFilter: 0 - ) + locationSettings: const LocationSettings(accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 0) ).listen((Position position) { - if (!_isSimulating) { - final now = DateTime.now(); - if (_lastUpdate == null || now.difference(_lastUpdate!).inMilliseconds > 500) { - _lastUpdate = now; - _updatePosition(LatLng(position.latitude, position.longitude), position.speed); - } + final now = DateTime.now(); + if (_lastUpdate == null || now.difference(_lastUpdate!).inMilliseconds > 500) { + _lastUpdate = now; + _updatePosition(LatLng(position.latitude, position.longitude), position.speed); } - }, onError: (error) { - debugPrint("${AppStrings.unknownDevice}: $error"); }); } void _updatePosition(LatLng newPoint, double speed) { if (!mounted) return; setState(() { - if (_routePoints.isNotEmpty) { - _totalDistance += Geolocator.distanceBetween(_currentPosition.latitude, _currentPosition.longitude, newPoint.latitude, newPoint.longitude); + if (_isRunning) { + if (_routePoints.isNotEmpty) { + _totalDistance += Geolocator.distanceBetween(_currentPosition.latitude, _currentPosition.longitude, newPoint.latitude, newPoint.longitude); + } + _routePoints.add(newPoint); + _updateTraveledPolylines(); + + // Lógica para consumir o trajeto planejado à medida que passamos por ele + if (_remainingPlannedPoints.isNotEmpty) { + while (_remainingPlannedPoints.length > 1) { + double distanceToPoint = Geolocator.distanceBetween( + newPoint.latitude, newPoint.longitude, + _remainingPlannedPoints[0].latitude, _remainingPlannedPoints[0].longitude + ); + // Se estivermos a menos de 15m do ponto do trajeto, removemo-lo + if (distanceToPoint < 15) { + _remainingPlannedPoints.removeAt(0); + } else { + break; + } + } + _updateRemainingPolyline(); + } + } else { + // Antes da corrida, mantém os últimos pontos para calcular a rotação + if (_routePoints.length >= 2) _routePoints.removeAt(0); + _routePoints.add(newPoint); } + _currentSpeed = speed >= 0 ? speed : 0; - _routePoints.add(newPoint); _currentPosition = newPoint; _updateMarkers(); - if (_routePoints.length > 1) { - _polylines.removeWhere((p) => p.polylineId.value == 'route' || p.polylineId.value == 'route_glow'); - _polylines.add(Polyline( - polylineId: const PolylineId('route_glow'), - points: List.from(_routePoints), - color: Colors.cyanAccent.withOpacity(0.3), - width: 14, - zIndex: 9, - )); - _polylines.add(Polyline( - polylineId: const PolylineId('route'), - points: List.from(_routePoints), - color: Colors.white, - width: 6, - patterns: [PatternItem.dot, PatternItem.gap(15)], - jointType: JointType.round, - zIndex: 10, - )); + if (_plannedEnd != null && _isRunning) { + double distToEnd = Geolocator.distanceBetween(newPoint.latitude, newPoint.longitude, _plannedEnd!.latitude, _plannedEnd!.longitude); + if (distToEnd < 15) { + _finishRun(); + } } }); @@ -167,16 +196,40 @@ class _GoogleMapScreenState extends State { } } + void _updateTraveledPolylines() { + if (_routePoints.length > 1) { + _polylines.removeWhere((p) => p.polylineId.value == 'route' || p.polylineId.value == 'route_glow'); + _polylines.add(Polyline(polylineId: const PolylineId('route_glow'), points: List.from(_routePoints), color: Colors.cyanAccent.withOpacity(0.3), width: 14, zIndex: 9)); + _polylines.add(Polyline(polylineId: const PolylineId('route'), points: List.from(_routePoints), color: Colors.white, width: 6, patterns: [PatternItem.dot, PatternItem.gap(15)], jointType: JointType.round, zIndex: 10)); + } + } + + void _updateRemainingPolyline() { + _polylines.removeWhere((p) => p.polylineId.value == 'planned_preview'); + if (_remainingPlannedPoints.isNotEmpty) { + // A linha coral começa na posição atual do usuário e segue o que resta + List pointsToDraw = [_currentPosition, ..._remainingPlannedPoints]; + _polylines.add(Polyline( + polylineId: const PolylineId('planned_preview'), + points: pointsToDraw, + color: AppColors.coral.withOpacity(0.5), + width: 4, + patterns: [PatternItem.dash(20), PatternItem.gap(10)], + zIndex: 8, + )); + } + } + void _updateMarkers() { _markers.removeWhere((m) => m.markerId.value == 'follower'); _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, + markerId: const MarkerId('follower'), + position: _currentPosition, + rotation: _calculateRotation(_routePoints), + flat: true, + anchor: const Offset(0.5, 0.5), + icon: _arrowIcon ?? BitmapDescriptor.defaultMarker, + zIndex: 12 )); } @@ -187,76 +240,298 @@ class _GoogleMapScreenState extends State { return Geolocator.bearingBetween(p1.latitude, p1.longitude, p2.latitude, p2.longitude); } - void _onMapTap(LatLng point) { + Future> _getRoutePolyline(LatLng start, LatLng end) async { + try { + PolylinePoints polylinePoints = PolylinePoints(); + PolylineResult result = await polylinePoints.getRouteBetweenCoordinates( + request: PolylineRequest( + origin: PointLatLng(start.latitude, start.longitude), + destination: PointLatLng(end.latitude, end.longitude), + mode: TravelMode.walking, + ), + googleApiKey: googleApiKey, + ); + + List coords = []; + if (result.points.isNotEmpty) { + for (var point in result.points) { + coords.add(LatLng(point.latitude, point.longitude)); + } + } + return coords; + } catch (e) { + debugPrint("Erro ao buscar rota: $e"); + return []; + } + } + + void _onMapTap(LatLng point) async { if (!_isPlanningMode) return; + setState(() { - if (_plannedStart == null) { - _plannedStart = point; - _markers.add(Marker(markerId: const MarkerId('planned_start'), position: point, icon: _startIcon ?? BitmapDescriptor.defaultMarker, zIndex: 5, infoWindow: const InfoWindow(title: AppStrings.startPoint))); - } else if (_plannedEnd == null) { - _plannedEnd = point; - _markers.add(Marker(markerId: const MarkerId('planned_end'), position: point, icon: _finishIcon ?? BitmapDescriptor.defaultMarker, zIndex: 5, infoWindow: const InfoWindow(title: AppStrings.finishPoint))); - _polylines.add(Polyline(polylineId: const PolylineId('planned_route'), points: [_plannedStart!, _plannedEnd!], color: Colors.white.withOpacity(0.1), width: 2, zIndex: 1)); + _plannedEnd = point; + _markers.removeWhere((m) => m.markerId.value == 'planned_end'); + _markers.add(Marker(markerId: const MarkerId('planned_end'), position: point, icon: _finishIcon ?? BitmapDescriptor.defaultMarker, zIndex: 5, infoWindow: InfoWindow(title: AppStrings.finishPoint))); + }); + + List streetPoints = await _getRoutePolyline(_currentPosition, point); + if (streetPoints.isEmpty) { + streetPoints = [_currentPosition, point]; + } + + setState(() { + _remainingPlannedPoints = List.from(streetPoints); + _updateRemainingPolyline(); + }); + + _showConfirmationDialog(); + } + + void _showStartOptions() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => Container( + margin: const EdgeInsets.all(20), + decoration: BoxDecoration(color: AppColors.backgroundGrey, borderRadius: BorderRadius.circular(30), border: Border.all(color: Colors.white10)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 15), + Container(width: 40, height: 4, decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(2))), + const SizedBox(height: 25), + _buildOptionTile(Icons.location_on_rounded, AppStrings.markDestination, () { + Navigator.pop(context); + setState(() => _isPlanningMode = true); + }), + const Divider(color: Colors.white10, indent: 20, endIndent: 20), + _buildOptionTile(Icons.history_rounded, AppStrings.chooseRoute, () { + Navigator.pop(context); + }), + const SizedBox(height: 20), + ], + ), + ), + ); + } + + void _showConfirmationDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppColors.backgroundGrey, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), + title: Text(AppStrings.confirmDestination, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + actions: [ + 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)), + ), + ], + ), + ); + } + + void _startCountdown() { + setState(() { + _isCountingDown = true; + _countdownValue = 3; + _isPlanningMode = false; + }); + + Timer.periodic(const Duration(seconds: 1), (timer) { + if (_countdownValue == 1) { + timer.cancel(); + _startRun(); } else { - _plannedStart = point; - _plannedEnd = null; - _markers.removeWhere((m) => m.markerId.value.startsWith('planned')); - _polylines.removeWhere((p) => p.polylineId.value == 'planned_route'); - _markers.add(Marker(markerId: const MarkerId('planned_start'), position: point, icon: _startIcon ?? BitmapDescriptor.defaultMarker, zIndex: 5, infoWindow: const InfoWindow(title: AppStrings.startPoint))); + setState(() => _countdownValue--); } }); } - void _toggleSimulation() { - if (_isSimulating) { - _simulationTimer?.cancel(); - setState(() => _isSimulating = false); - } else { - setState(() { - _isSimulating = true; - _isPlanningMode = false; - _routePoints.clear(); - _polylines.removeWhere((p) => p.polylineId.value == 'route' || p.polylineId.value == 'route_glow'); - _totalDistance = 0.0; - _currentPosition = _plannedStart ?? _currentPosition; - _routePoints.add(_currentPosition); - _updateMarkers(); - }); + void _startRun() { + setState(() { + _isCountingDown = false; + _isRunning = true; + _totalDistance = 0.0; + _secondsElapsed = 0; + _routePoints.clear(); + _routePoints.add(_currentPosition); + }); - _simulationTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) { - double latStep = 0.000025; - double lngStep = 0.000025; + _stopwatchTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + setState(() => _secondsElapsed++); + }); + } - if (_plannedEnd != null) { - double dist = Geolocator.distanceBetween(_currentPosition.latitude, _currentPosition.longitude, _plannedEnd!.latitude, _plannedEnd!.longitude); - if (dist < 2) { - _updatePosition(_plannedEnd!, 0.0); - _toggleSimulation(); - return; - } - latStep = (_plannedEnd!.latitude - _currentPosition.latitude) / (max(dist / 0.8, 1)); - lngStep = (_plannedEnd!.longitude - _currentPosition.longitude) / (max(dist / 0.8, 1)); - } + void _finishRun() { + _stopwatchTimer?.cancel(); + setState(() { + _isRunning = false; + _remainingPlannedPoints.clear(); + _polylines.removeWhere((p) => p.polylineId.value == 'planned_preview'); + }); - LatLng nextPoint = LatLng(_currentPosition.latitude + latStep, _currentPosition.longitude + lngStep); - _updatePosition(nextPoint, 3.5 + Random().nextDouble() * 0.5); - }); - } + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + backgroundColor: AppColors.backgroundGrey, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.check_circle_rounded, color: Colors.greenAccent, size: 70), + const SizedBox(height: 20), + Text(AppStrings.runFinished, style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w900)), + const SizedBox(height: 25), + _buildResultRow(AppStrings.totalDistance, _formatDistance(_totalDistance)), + const SizedBox(height: 15), + _buildResultRow(AppStrings.totalTime, _formatTime(_secondsElapsed)), + const SizedBox(height: 30), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom(backgroundColor: AppColors.coral, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), + child: Text(AppStrings.close, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ), + ); + } + + String _formatDistance(double meters) { + if (meters < 1000) return "${meters.toStringAsFixed(0)} ${AppStrings.metersUnit}"; + return "${(meters / 1000).toStringAsFixed(2)} ${AppStrings.kmUnit}"; + } + + String _formatTime(int seconds) { + int hours = seconds ~/ 3600; + int minutes = (seconds % 3600) ~/ 60; + int remainingSeconds = seconds % 60; + return "${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}"; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, - appBar: AppBar(title: Text(_isPlanningMode ? AppStrings.mapTitlePlanning : AppStrings.mapTitleRunning, style: const TextStyle(fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 2)), centerTitle: true, backgroundColor: Colors.transparent, elevation: 0, leading: IconButton(icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white), onPressed: () => Navigator.pop(context)), actions: [IconButton(icon: Icon(_isPlanningMode ? Icons.check_circle_rounded : Icons.add_location_alt_rounded, color: AppColors.coral, size: 30), onPressed: () => setState(() => _isPlanningMode = !_isPlanningMode))]), + appBar: AppBar(title: Text(_isRunning ? AppStrings.mapTitleRunning : AppStrings.mapTitlePlanning, style: const TextStyle(fontWeight: FontWeight.w900, color: Colors.white, letterSpacing: 2)), centerTitle: true, backgroundColor: Colors.transparent, elevation: 0, leading: IconButton(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.7, decoration: BoxDecoration(color: AppColors.backgroundGrey, borderRadius: BorderRadius.circular(55), border: Border.all(color: Colors.white.withOpacity(0.1), width: 3), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.7), blurRadius: 50, offset: const Offset(0, 30))]), 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, onTap: _onMapTap, markers: _markers, polylines: _polylines, zoomControlsEnabled: false, myLocationButtonEnabled: false, compassEnabled: false, mapToolbarEnabled: false)))), Positioned(top: 115, left: 45, right: 45, child: Container(padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 25), decoration: BoxDecoration(color: AppColors.background.withOpacity(0.95), borderRadius: BorderRadius.circular(25), border: Border.all(color: Colors.white10), boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 10)]), child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [_buildStat(AppStrings.mapPace, "${(_currentSpeed * 3.6).toStringAsFixed(1)}", AppStrings.kmhUnit), Container(width: 1, height: 35, color: Colors.white10), _buildStat(AppStrings.mapRoute, (_totalDistance / 1000).toStringAsFixed(2), AppStrings.kmUnit)]))), if (_isPlanningMode) Positioned(bottom: 140, left: 60, right: 60, child: Container(padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: Colors.black.withOpacity(0.85), borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.coral.withOpacity(0.5))), child: const Text(AppStrings.planningInstruction, textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold))))]), - floatingActionButton: FloatingActionButton.extended(onPressed: _toggleSimulation, label: Text(_isSimulating ? AppStrings.btnStop : AppStrings.btnStartRun, style: const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.5)), icon: Icon(_isSimulating ? Icons.stop_rounded : Icons.play_arrow_rounded, size: 32), backgroundColor: _isSimulating ? AppColors.coral : Colors.white, foregroundColor: _isSimulating ? Colors.white : AppColors.background, elevation: 15), + body: Stack( + children: [ + Center( + child: Container( + width: MediaQuery.of(context).size.width * 0.94, height: MediaQuery.of(context).size.height * 0.7, + 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)]), + 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, onTap: _onMapTap, markers: _markers, polylines: _polylines, zoomControlsEnabled: false, myLocationButtonEnabled: false, compassEnabled: false, mapToolbarEnabled: false)) + ), + ), + + Positioned( + top: 115, left: 20, right: 20, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 20), + decoration: BoxDecoration(color: AppColors.background.withOpacity(0.95), borderRadius: BorderRadius.circular(25), border: Border.all(color: Colors.white10)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildStat(AppStrings.mapPace, (_currentSpeed * 3.6).toStringAsFixed(1), AppStrings.kmhUnit), + _buildDivider(), + _buildStat(AppStrings.mapRoute, _formatDistanceValue(_totalDistance), _totalDistance < 1000 ? AppStrings.metersUnit : AppStrings.kmUnit), + _buildDivider(), + _buildStat(AppStrings.mapTime, _formatTimeShort(_secondsElapsed), ""), + ], + ), + ), + ), + + if (_isCountingDown) + Container( + color: Colors.black54, + child: Center( + child: Text("$_countdownValue", style: const TextStyle(color: Colors.white, fontSize: 150, fontWeight: FontWeight.w900)), + ), + ), + + if (_isPlanningMode) + Positioned( + bottom: 120, left: 60, right: 60, + child: Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration(color: AppColors.coral.withOpacity(0.9), borderRadius: BorderRadius.circular(20)), + child: Text(AppStrings.markDestination, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: _isRunning ? _finishRun : _showStartOptions, + label: Text(_isRunning ? AppStrings.btnStop : AppStrings.btnStartRun, style: const TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.5)), + icon: Icon(_isRunning ? Icons.stop_rounded : Icons.play_arrow_rounded, size: 32), + backgroundColor: _isRunning ? AppColors.coral : Colors.white, + foregroundColor: _isRunning ? Colors.white : AppColors.background, + elevation: 15, + ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } - 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)), const SizedBox(height: 6), Row(crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [Text(value, style: const TextStyle(color: Colors.white, fontSize: 26, fontWeight: FontWeight.w900)), const SizedBox(width: 3), Text(unit, style: const TextStyle(color: Colors.white54, fontSize: 10, fontWeight: FontWeight.bold))])]); + Widget _buildOptionTile(IconData icon, String title, VoidCallback onTap) { + return ListTile( + leading: Container(padding: const EdgeInsets.all(8), decoration: BoxDecoration(color: Colors.white.withOpacity(0.05), shape: BoxShape.circle), child: Icon(icon, color: AppColors.coral)), + title: Text(title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + trailing: const Icon(Icons.arrow_forward_ios_rounded, color: Colors.white24, size: 16), + onTap: onTap, + ); } -} \ No newline at end of file + + Widget _buildResultRow(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(color: Colors.white54, fontWeight: FontWeight.bold)), + Text(value, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w900)), + ], + ); + } + + String _formatDistanceValue(double meters) { + if (meters < 1000) return meters.toStringAsFixed(0); + return (meters / 1000).toStringAsFixed(2); + } + + String _formatTimeShort(int seconds) { + int mins = seconds ~/ 60; + int secs = seconds % 60; + return "${mins.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}"; + } + + Widget _buildDivider() => Container(width: 1, height: 30, 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: 9, fontWeight: FontWeight.w900, letterSpacing: 1)), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text(value, style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w900)), + if (unit.isNotEmpty) ...[const SizedBox(width: 2), Text(unit, style: const TextStyle(color: Colors.white54, fontSize: 9, fontWeight: FontWeight.bold))], + ], + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 5ea83d4..889f011 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.9" args: dependency: transitive description: @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: dart_jsonwebtoken - sha256: "0de65691c1d736e9459f22f654ddd6fd8368a271d4e41aa07e53e6301eff5075" + sha256: c6ecb3bb991c459b91c5adf9e871113dcb32bbe8fe7ca2c92723f88ffc1e0b7a url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.3.2" dbus: dependency: transitive description: @@ -278,6 +278,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.33" + flutter_polyline_points: + dependency: "direct main" + description: + name: flutter_polyline_points + sha256: "3a1c8c30abee9fb0fbe44c70d5d1cedb10ef28ec7ea285c669f02b3e183483aa" + url: "https://pub.dev" + source: hosted + version: "2.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -364,10 +372,10 @@ packages: dependency: "direct main" description: name: google_maps_flutter - sha256: "9b0d6dab3de6955837575dc371dd772fcb5d0a90f6a4954e8c066472f9938550" + sha256: "85339d0c9b5c7bac305df0bc1bc72a990f7f59b1846f048c96c022110d5d4a69" url: "https://pub.dev" source: hosted - version: "2.14.2" + version: "2.15.0" google_maps_flutter_android: dependency: transitive description: @@ -380,10 +388,10 @@ packages: dependency: transitive description: name: google_maps_flutter_ios - sha256: "174d730bc3f253e1c06a342d7a5efb216f15003a6e26693c2d70d60973625af4" + sha256: c855600dce17e77e8af96edcf85cb68501675bb77a72f85009d08c17a8805ace url: "https://pub.dev" source: hosted - version: "2.17.5" + version: "2.18.0" google_maps_flutter_platform_interface: dependency: transitive description: @@ -396,10 +404,10 @@ packages: dependency: transitive description: name: google_maps_flutter_web - sha256: d416602944e1859f3cbbaa53e34785c223fa0a11eddb34a913c964c5cbb5d8cf + sha256: "2321d24f8bce2d01c441b9fd7835a6032ea44b3f0bfcf93bcebd9740fb5a85cd" url: "https://pub.dev" source: hosted - version: "0.5.14+3" + version: "0.6.1" gotrue: dependency: transitive description: @@ -420,10 +428,10 @@ packages: dependency: transitive description: name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" html: dependency: transitive description: @@ -540,18 +548,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -580,10 +588,10 @@ packages: dependency: transitive description: name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f" url: "https://pub.dev" source: hosted - version: "0.17.4" + version: "0.17.5" objective_c: dependency: transitive description: @@ -740,10 +748,10 @@ packages: dependency: transitive description: name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.5.0" postgrest: dependency: transitive description: @@ -812,10 +820,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" url: "https://pub.dev" source: hosted - version: "2.4.20" + version: "2.4.21" shared_preferences_foundation: dependency: transitive description: @@ -937,10 +945,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" typed_data: dependency: transitive description: @@ -977,10 +985,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: b1aca26728b7cc7a3af971bb6f601554a8ae9df2e0a006de8450ba06a17ad36a + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.4.0" + version: "6.4.1" url_launcher_linux: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 540bafb..6179f80 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: permission_handler: ^11.3.1 lottie: ^3.1.2 supabase_flutter: ^2.6.0 + flutter_polyline_points: ^2.1.0 dev_dependencies: flutter_test: