diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 399f698..ebbf428 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -3,5 +3,7 @@ the Flutter tool needs it to communicate with the running application to allow setting breakpoints, to provide hot reload, etc. --> + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e7eecc9..b099f59 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,11 +1,5 @@ - - - - - - - + diff --git a/lib/main.dart b/lib/main.dart index 305c70a..275b9a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'constants/app_colors.dart'; import 'screens/google_map_screen.dart'; -import 'bluetooth_screen.dart'; +import 'screens/bluetooth_connection_screen.dart'; // Importando a nova tela void main() { - // O ponto de entrada do aplicativo. + // Ponto de entrada do aplicativo. runApp(const MyApp()); } @@ -34,38 +34,54 @@ class RunningScreen extends StatefulWidget { class _RunningScreenState extends State with SingleTickerProviderStateMixin { // Variáveis de estado para controlar os dados da corrida. - double progress = 0.0; // Progresso de 0.0 a 1.0 (ex: 0.5 = 50%). + double progress = 0.35; // Progresso inicial simulado para estética. double targetDistance = 8.0; // Distância alvo em KM. - double currentDistance = 0.0; // Distância atual percorrida. + double currentDistance = 2.8; // Distância atual percorrida simulada. - /// Constrói o indicador de progresso circular central. + /// Constrói o indicador de progresso circular central com melhorias estéticas. Widget _buildCircularProgressIndicator() { return SizedBox( - width: 200, - height: 200, + width: 210, + height: 210, child: Stack( alignment: Alignment.center, children: [ + // Efeito de brilho (glow) sutil atrás do progresso. + Container( + width: 180, + height: 180, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: AppColors.white.withOpacity(0.05), + blurRadius: 30, + spreadRadius: 10, + ), + ], + ), + ), // TweenAnimationBuilder cria uma animação suave quando o valor do progresso muda. TweenAnimationBuilder( tween: Tween(begin: 0.0, end: progress), - duration: const Duration(milliseconds: 500), + duration: const Duration(seconds: 1), + curve: Curves.easeInOut, builder: (context, value, _) { return CustomPaint( - size: const Size(200, 200), + size: const Size(210, 210), painter: CircularProgressPainter( progress: value, - strokeWidth: 12, + strokeWidth: 14, progressColor: AppColors.white, - backgroundColor: AppColors.white.withOpacity(0.3), + backgroundColor: AppColors.white.withOpacity(0.15), ), ); }, ), - // Círculo interno cinza que contém o texto da porcentagem. + // Círculo interno que contém o texto da porcentagem. Container( - width: 170, - height: 170, + width: 175, + height: 175, decoration: const BoxDecoration( color: AppColors.backgroundGrey, shape: BoxShape.circle, @@ -76,14 +92,20 @@ class _RunningScreenState extends State Text( "${(progress * 100).toInt()}%", style: const TextStyle( - fontSize: 36, - fontWeight: FontWeight.bold, + fontSize: 42, + fontWeight: FontWeight.w900, color: Colors.white, + letterSpacing: -1, ), ), - const Text( + Text( "COMPLETO", - style: TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle( + color: Colors.white.withOpacity(0.5), + fontSize: 11, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), ), ], ), @@ -96,112 +118,142 @@ class _RunningScreenState extends State @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: - AppColors.background, // Cor de fundo escura definida nas constantes. + backgroundColor: AppColors.background, // Cor de fundo escura definida nas constantes. body: Stack( children: [ // 1. Indicador de progresso circular posicionado no topo central. Align( alignment: Alignment.topCenter, child: Padding( - padding: const EdgeInsets.only(top: 60), + padding: const EdgeInsets.only(top: 70), child: _buildCircularProgressIndicator(), ), ), - // 2. Exibição da distância (ex: 0.0 KM | 8.0 KM). + // 2. Exibição da distância estilizada como um badge. Positioned( - top: 280, + top: 300, left: 0, right: 0, child: Center( child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 8, - ), + padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 10), decoration: BoxDecoration( - color: AppColors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(20), + color: Colors.white.withOpacity(0.08), + borderRadius: BorderRadius.circular(25), + border: Border.all(color: Colors.white.withOpacity(0.1)), ), child: Text( "${currentDistance.toStringAsFixed(1)} KM | ${targetDistance.toStringAsFixed(1)} KM", style: const TextStyle( color: AppColors.white, - fontSize: 16, - fontWeight: FontWeight.bold, + fontSize: 15, + fontWeight: FontWeight.w800, + letterSpacing: 0.5, ), ), ), ), ), - // 3. Contêiner de estatísticas (Passos, BPM, K/CAL) e o mapa simulado. + // 3. Contêiner de estatísticas e o mapa interativo. Positioned( - top: 340, + top: 360, left: 20, right: 20, child: Container( - height: 200, + height: 210, decoration: BoxDecoration( color: AppColors.backgroundGrey, - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], ), child: Row( children: [ - // Coluna esquerda com ícones e valores de estatística. + // Lado Esquerdo: Estatísticas. Expanded( flex: 4, child: Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildStatItem( - Icons.directions_run, - "3219", - "PASSOS", - ), - const Divider(color: Colors.white24, height: 1), - _buildStatItem(Icons.favorite_border, "98", "BPM"), - const Divider(color: Colors.white24, height: 1), - _buildStatItem( - Icons.local_fire_department, - "480", - "K/CAL", - ), + _buildStatItem(Icons.directions_run_rounded, "3219", "PASSOS"), + Divider(color: Colors.white.withOpacity(0.1), height: 1), + _buildStatItem(Icons.favorite_rounded, "98", "BPM"), + Divider(color: Colors.white.withOpacity(0.1), height: 1), + _buildStatItem(Icons.local_fire_department_rounded, "480", "K/CAL"), ], ), ), ), - // Coluna direita contendo o desenho do mapa simulado. + // Lado Direito: Miniatura do Mapa Clicável. Expanded( flex: 6, child: GestureDetector( onTap: () { Navigator.push( context, - MaterialPageRoute( - builder: (context) => const GoogleMapScreen(), - ), + MaterialPageRoute(builder: (context) => const GoogleMapScreen()), ); }, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topRight: Radius.circular(24), - bottomRight: Radius.circular(24), - ), - child: Stack( - children: [ - Container( - color: const Color(0xFF3A3A3C), - ), // Fundo do mapa. - CustomPaint( - size: Size.infinite, - painter: - MapPainter(), // Desenha as linhas e o marcador. - ), - ], + child: Container( + margin: const EdgeInsets.all(8), + child: ClipRRect( + borderRadius: BorderRadius.circular(22), + child: Stack( + children: [ + Container(color: const Color(0xFF2C2C2E)), + CustomPaint( + size: Size.infinite, + painter: MapPainter(), + ), + // Overlay estético indicando interatividade. + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.4), + ], + ), + ), + ), + Positioned( + bottom: 12, + right: 12, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: AppColors.background.withOpacity(0.8), + shape: BoxShape.circle, + ), + child: const Icon(Icons.fullscreen_rounded, color: Colors.white, size: 20), + ), + ), + const Positioned( + top: 12, + left: 12, + child: Text( + "MAPA", + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w900, + letterSpacing: 1, + ), + ), + ), + ], + ), ), ), ), @@ -211,104 +263,55 @@ class _RunningScreenState extends State ), ), - // 4. Barra de progresso linear (centralizada acima dos botões inferiores). + // 4. Barra de progresso linear centralizada. Positioned( - bottom: 160, - left: - 40, // Espaçamento igual na esquerda e direita para centralizar. - right: 40, + bottom: 170, + left: 50, + right: 50, child: ClipRRect( - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(10), child: LinearProgressIndicator( value: progress, - minHeight: 12, - backgroundColor: AppColors.backgroundGrey.withOpacity(0.3), - valueColor: const AlwaysStoppedAnimation( - AppColors.white, - ), + minHeight: 8, + backgroundColor: Colors.white.withOpacity(0.1), + valueColor: const AlwaysStoppedAnimation(AppColors.white), ), ), ), - // 5. Botões de menu inferiores. + // 5. Menu de navegação inferior. Positioned( - bottom: 60, + bottom: 50, left: 0, right: 0, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildMenuButton(Icons.settings, 'Configurações clicado!'), - _buildMenuButton(Icons.group_outlined, 'Grupos clicado!'), - _buildMenuButton(Icons.access_time, 'Histórico clicado!'), - _buildMenuButton( - Icons.notifications_none, - 'Notificações clicado!', - showBadge: true, - ), - _buildMenuButton( - Icons.person, - 'Perfil clicado!', - isAvatar: true, - ), + _buildMenuButton(Icons.settings_outlined, 'Configurações'), + _buildMenuButton(Icons.group_outlined, 'Grupos'), + _buildMenuButton(Icons.history_rounded, 'Histórico'), + _buildMenuButton(Icons.notifications_none_rounded, 'Notificações', showBadge: true), + _buildMenuButton(Icons.person_outline_rounded, 'Perfil', isAvatar: true), ], ), ), - // 6. Botão de Bluetooth no canto superior direito. + // 6. Ação rápida de Bluetooth. Positioned( top: 60, - right: 30, - child: GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BluetoothScreen(), - ), - ); - }, - child: Container( - padding: const EdgeInsets.all(8), - decoration: const BoxDecoration( - color: AppColors.backgroundGrey, - shape: BoxShape.circle, - ), - child: Stack( - children: [ - const Icon( - Icons.bluetooth, - color: AppColors.white, - size: 20, - ), - // Pontinho vermelho indicando status ou notificação. - Positioned( - left: 0, - bottom: 0, - child: Container( - width: 6, - height: 6, - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - ), - ), - ], - ), - ), - ), + right: 25, + child: _buildSmallActionButton(Icons.bluetooth, Colors.red), ), ], ), ); } - /// Constrói uma linha de estatística com ícone, valor e rótulo. + /// Item de estatística com design refinado. Widget _buildStatItem(IconData icon, String value, String label) { return Row( children: [ - Icon(icon, color: Colors.white70, size: 24), + Icon(icon, color: AppColors.coral.withOpacity(0.8), size: 22), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -318,13 +321,18 @@ class _RunningScreenState extends State value, style: const TextStyle( color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, + fontSize: 19, + fontWeight: FontWeight.w900, ), ), Text( label, - style: const TextStyle(color: Colors.white60, fontSize: 10), + style: TextStyle( + color: Colors.white.withOpacity(0.4), + fontSize: 9, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), ), ], ), @@ -332,55 +340,53 @@ class _RunningScreenState extends State ); } - /// Constrói um botão de menu clicável que exibe um SnackBar. - Widget _buildMenuButton( - IconData icon, - String message, { - bool showBadge = false, - bool isAvatar = false, - }) { + /// Botões do menu com melhorias visuais. + Widget _buildMenuButton(IconData icon, String message, {bool showBadge = false, bool isAvatar = false}) { return GestureDetector( onTap: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), + behavior: SnackBarBehavior.floating, duration: const Duration(seconds: 1), ), ); }, child: Stack( + clipBehavior: Clip.none, children: [ - // Exibe um avatar circular ou um ícone padrão. isAvatar - ? CircleAvatar( - radius: 20, - backgroundColor: Colors.orange, + ? Container( + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: AppColors.coral, width: 2), + ), child: const CircleAvatar( radius: 18, - backgroundImage: NetworkImage( - 'https://i.pravatar.cc/150?u=1', - ), + backgroundImage: NetworkImage('https://i.pravatar.cc/150?u=1'), ), ) : Container( - padding: const EdgeInsets.all(10), - decoration: const BoxDecoration( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( color: AppColors.backgroundGrey, - shape: BoxShape.circle, + borderRadius: BorderRadius.circular(18), + border: Border.all(color: Colors.white.withOpacity(0.05)), ), - child: Icon(icon, color: AppColors.white, size: 24), + child: Icon(icon, color: Colors.white.withOpacity(0.9), size: 24), ), - // Exibe um pontinho vermelho de notificação se showBadge for true. if (showBadge) Positioned( - left: 0, - bottom: 0, + right: -2, + top: -2, child: Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - color: Colors.red, + width: 10, + height: 10, + decoration: BoxDecoration( + color: AppColors.coral, shape: BoxShape.circle, + border: Border.all(color: AppColors.background, width: 2), ), ), ), @@ -388,68 +394,84 @@ class _RunningScreenState extends State ), ); } + + /// Botão de ação rápida (Bluetooth). + Widget _buildSmallActionButton(IconData icon, Color badgeColor) { + return GestureDetector( + onTap: () { + // Navegando para a nova tela de conexão Bluetooth + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const BluetoothConnectionScreen()), + ); + }, + child: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.backgroundGrey, + shape: BoxShape.circle, + border: Border.all(color: Colors.white.withOpacity(0.05)), + ), + child: Stack( + children: [ + Icon(icon, color: Colors.white, size: 20), + Positioned( + right: 0, + top: 0, + child: Container( + width: 7, + height: 7, + decoration: BoxDecoration( + color: badgeColor, + shape: BoxShape.circle, + border: Border.all(color: AppColors.backgroundGrey, width: 1.5), + ), + ), + ), + ], + ), + ), + ); + } } -/// Pintor customizado para desenhar o traçado do mapa simulado. +/// Pintor customizado para o mapa miniatura estético. class MapPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = Colors.white38 + final paintPath = Paint() + ..color = AppColors.coral.withOpacity(0.5) ..strokeWidth = 3 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; - // Desenha a linha sinuosa do percurso. final path = Path(); path.moveTo(size.width * 0.1, size.height * 0.8); - path.quadraticBezierTo( - size.width * 0.3, - size.height * 0.7, - size.width * 0.4, - size.height * 0.4, - ); - path.quadraticBezierTo( - size.width * 0.5, - size.height * 0.1, - size.width * 0.7, - size.height * 0.3, - ); - path.lineTo(size.width * 0.9, size.height * 0.2); + path.quadraticBezierTo(size.width * 0.3, size.height * 0.9, size.width * 0.5, size.height * 0.5); + path.quadraticBezierTo(size.width * 0.7, size.height * 0.1, size.width * 0.9, size.height * 0.3); - // Desenha uma "estrada" mais grossa branca. - final roadPaint = Paint() - ..color = Colors.white - ..strokeWidth = 8 + final paintRoad = Paint() + ..color = Colors.white.withOpacity(0.1) + ..strokeWidth = 10 ..style = PaintingStyle.stroke; - final roadPath = Path(); - roadPath.moveTo(size.width * 0.6, size.height * 1.1); - roadPath.quadraticBezierTo( - size.width * 0.7, - size.height * 0.8, - size.width * 1.1, - size.height * 0.7, - ); + final road = Path(); + road.moveTo(0, size.height * 0.5); + road.lineTo(size.width, size.height * 0.6); - canvas.drawPath(path, paint); - canvas.drawPath(roadPath, roadPaint); + canvas.drawPath(road, paintRoad); + canvas.drawPath(path, paintPath); - // Desenha o marcador circular (o pino no mapa). - final markerPaint = Paint()..color = const Color(0xFFFF6B6B); - final markerPos = Offset(size.width * 0.4, size.height * 0.4); - canvas.drawCircle(markerPos, 6, markerPaint); - - // Desenha o centro branco do marcador. - final innerPaint = Paint()..color = Colors.white; - canvas.drawCircle(markerPos, 2, innerPaint); + final markerPaint = Paint()..color = AppColors.coral; + canvas.drawCircle(Offset(size.width * 0.5, size.height * 0.5), 5, markerPaint); + canvas.drawCircle(Offset(size.width * 0.5, size.height * 0.5), 8, Paint()..color = AppColors.coral.withOpacity(0.3)); } @override bool shouldRepaint(CustomPainter oldDelegate) => false; } -/// Pintor customizado para desenhar o arco de progresso circular. +/// Pintor customizado para o arco de progresso circular. class CircularProgressPainter extends CustomPainter { final double progress; final double strokeWidth; @@ -468,31 +490,21 @@ class CircularProgressPainter extends CustomPainter { final center = Offset(size.width / 2, size.height / 2); final radius = (size.width - strokeWidth) / 2; - // Desenha o círculo de fundo (cinza transparente). - final backgroundPaint = Paint() + canvas.drawCircle(center, radius, Paint() ..color = backgroundColor ..strokeWidth = strokeWidth - ..style = PaintingStyle.stroke; + ..style = PaintingStyle.stroke); - canvas.drawCircle(center, radius, backgroundPaint); - - // Desenha o arco de progresso (branco). final progressPaint = Paint() ..color = progressColor ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; - const startAngle = -3.14159265359 / 2; // Começa no topo (-90 graus). - final sweepAngle = - 2 * - 3.14159265359 * - progress; // Define o tamanho do arco com base no progresso. - canvas.drawArc( Rect.fromCircle(center: center, radius: radius), - startAngle, - sweepAngle, + -1.5708, // -90 graus em radianos + 6.2831 * progress, // 360 graus em radianos * progresso false, progressPaint, ); diff --git a/lib/screens/bluetooth_connection_screen.dart b/lib/screens/bluetooth_connection_screen.dart new file mode 100644 index 0000000..92be200 --- /dev/null +++ b/lib/screens/bluetooth_connection_screen.dart @@ -0,0 +1,373 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:permission_handler/permission_handler.dart'; +import '../constants/app_colors.dart'; + +class BluetoothConnectionScreen extends StatefulWidget { + const BluetoothConnectionScreen({super.key}); + + @override + State createState() => _BluetoothConnectionScreenState(); +} + +class _BluetoothConnectionScreenState extends State { + List _scanResults = []; + bool _isScanning = false; + BluetoothDevice? _connectedDevice; // Track connected device + late StreamSubscription> _scanResultsSubscription; + late StreamSubscription _isScanningSubscription; + + @override + void initState() { + super.initState(); + + _scanResultsSubscription = FlutterBluePlus.scanResults.listen((results) { + if (mounted) { + setState(() { + // FILTRO: Mantém apenas dispositivos que possuem um nome identificado + _scanResults = results.where((r) => r.device.platformName.isNotEmpty).toList(); + }); + } + }); + + _isScanningSubscription = FlutterBluePlus.isScanning.listen((state) { + if (mounted) { + setState(() { + _isScanning = state; + }); + } + }); + } + + @override + void dispose() { + _scanResultsSubscription.cancel(); + _isScanningSubscription.cancel(); + super.dispose(); + } + + Future _requestPermissionsAndStartScan() async { + if (Platform.isAndroid) { + Map statuses = await [ + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.location, + ].request(); + + if (statuses[Permission.bluetoothScan]!.isGranted && + statuses[Permission.bluetoothConnect]!.isGranted) { + _startScan(); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Permissões de Bluetooth negadas.'), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + } else { + _startScan(); + } + } + + Future _startScan() async { + try { + if (await FlutterBluePlus.adapterState.first != BluetoothAdapterState.on) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Ligue o Bluetooth para buscar dispositivos.'), + behavior: SnackBarBehavior.floating, + ), + ); + } + return; + } + + await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15)); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erro ao iniciar scan: $e'), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + } + + Future _stopScan() async { + try { + await FlutterBluePlus.stopScan(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erro ao parar scan: $e'), + behavior: SnackBarBehavior.floating, + ), + ); + } + } + } + + Future _connectToDevice(BluetoothDevice device) async { + try { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Conectando a ${device.platformName}...'), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + + await device.connect(); + if (mounted) { + setState(() { + _connectedDevice = device; + _isScanning = false; + }); + FlutterBluePlus.stopScan(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Conectado com sucesso!'), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erro ao conectar: $e'), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + ), + ); + } + } + } + + Future _disconnectDevice() async { + if (_connectedDevice != null) { + await _connectedDevice!.disconnect(); + setState(() { + _connectedDevice = null; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.background, + appBar: AppBar( + title: const Text( + 'DISPOSITIVOS', + style: TextStyle( + fontWeight: FontWeight.w900, + letterSpacing: 2, + color: Colors.white, + ), + ), + centerTitle: true, + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 20), + onPressed: () => Navigator.pop(context), + ), + ), + body: Stack( + children: [ + Column( + children: [ + const SizedBox(height: 20), + // Header Card + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: Container( + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + color: AppColors.backgroundGrey, + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white.withOpacity(0.05)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _connectedDevice != null + ? 'STATUS: CONECTADO' + : (_isScanning ? 'STATUS: BUSCANDO...' : 'STATUS: PRONTO'), + style: TextStyle( + color: _connectedDevice != null ? Colors.greenAccent : (_isScanning ? AppColors.coral : Colors.white54), + fontSize: 10, + fontWeight: FontWeight.w900, + letterSpacing: 1, + ), + ), + const SizedBox(height: 8), + Text( + _connectedDevice != null + ? '1 Dispositivo' + : '${_scanResults.length} Encontrados', + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + if (_connectedDevice == null) + GestureDetector( + onTap: _isScanning ? _stopScan : _requestPermissionsAndStartScan, + child: Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: _isScanning ? Colors.red.withOpacity(0.1) : AppColors.coral.withOpacity(0.1), + shape: BoxShape.circle, + border: Border.all( + color: _isScanning ? Colors.red.withOpacity(0.5) : AppColors.coral.withOpacity(0.5), + width: 2, + ), + ), + child: Icon( + _isScanning ? Icons.stop_rounded : Icons.search_rounded, + color: _isScanning ? Colors.red : AppColors.coral, + size: 28, + ), + ), + ) + else + GestureDetector( + onTap: _disconnectDevice, + child: Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + shape: BoxShape.circle, + border: Border.all( + color: Colors.red.withOpacity(0.5), + width: 2, + ), + ), + child: const Icon( + Icons.close_rounded, + color: Colors.red, + size: 28, + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 30), + // Device List + Expanded( + child: ListView.builder( + itemCount: _connectedDevice != null ? 1 : _scanResults.length, + padding: const EdgeInsets.symmetric(horizontal: 25), + physics: const BouncingScrollPhysics(), + itemBuilder: (context, index) { + final BluetoothDevice device = _connectedDevice ?? _scanResults[index].device; + final name = device.platformName; + + return Padding( + padding: const EdgeInsets.only(bottom: 18), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColors.backgroundGrey.withOpacity(0.4), + borderRadius: BorderRadius.circular(25), + border: Border.all(color: Colors.white.withOpacity(0.03)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.background.withOpacity(0.8), + borderRadius: BorderRadius.circular(15), + ), + child: Icon( + Icons.bluetooth_audio_rounded, + color: _connectedDevice != null ? Colors.greenAccent : AppColors.coral.withOpacity(0.8), + size: 24 + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w900 + ), + ), + const SizedBox(height: 4), + Text( + device.remoteId.toString(), + style: const TextStyle( + color: Colors.white38, + fontSize: 11, + fontWeight: FontWeight.bold + ), + ), + ], + ), + ), + if (_connectedDevice == null) + GestureDetector( + onTap: () => _connectToDevice(device), + child: const Icon(Icons.add_link_rounded, color: Colors.white24), + ) + else + const Icon(Icons.check_circle_rounded, color: Colors.greenAccent), + ], + ), + ), + ); + }, + ), + ), + ], + ), + if (_isScanning) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: LinearProgressIndicator( + backgroundColor: Colors.transparent, + valueColor: const AlwaysStoppedAnimation(AppColors.coral), + minHeight: 2, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/google_map_screen.dart b/lib/screens/google_map_screen.dart index c23be17..506adc5 100644 --- a/lib/screens/google_map_screen.dart +++ b/lib/screens/google_map_screen.dart @@ -1,5 +1,11 @@ +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 '../constants/app_colors.dart'; class GoogleMapScreen extends StatefulWidget { const GoogleMapScreen({super.key}); @@ -9,27 +15,248 @@ class GoogleMapScreen extends StatefulWidget { } class _GoogleMapScreenState extends State { - late GoogleMapController mapController; + GoogleMapController? _mapController; + StreamSubscription? _positionStreamSubscription; + Timer? _simulationTimer; - final LatLng _center = const LatLng(38.7223, -9.1393); // Lisbon coordinates + // Controle de frequência de atualização para evitar sobrecarga e crashes + DateTime? _lastUpdate; - void _onMapCreated(GoogleMapController controller) { - mapController = controller; + final List _routePoints = []; + final Set _polylines = {}; + final Set _markers = {}; + + LatLng? _plannedStart; + LatLng? _plannedEnd; + bool _isPlanningMode = false; + + double _currentSpeed = 0.0; + double _totalDistance = 0.0; + LatLng _currentPosition = const LatLng(38.7223, -9.1393); + bool _isLoading = true; + bool _isSimulating = false; + + BitmapDescriptor? _startIcon; + BitmapDescriptor? _arrowIcon; + BitmapDescriptor? _finishIcon; + + @override + void initState() { + super.initState(); + _setupIconsAndTracking(); + } + + 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); + 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); + 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.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()); + } + + Future _createArrowMarker(Color color, Color borderColor, double size) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final Path path = Path(); + path.moveTo(size / 2, 0); + path.lineTo(size * 0.9, size); + 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, Paint()..color = color); + canvas.drawPath(path, Paint()..color = borderColor..style = ui.PaintingStyle.stroke..strokeWidth = 7); + 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()); + } + + @override + void dispose() { + _positionStreamSubscription?.cancel(); + _simulationTimer?.cancel(); + _mapController?.dispose(); + super.dispose(); + } + + Future _initTracking() async { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) return; + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) return; + } + Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.bestForNavigation); + _currentPosition = LatLng(position.latitude, position.longitude); + setState(() => _isLoading = false); + _startLocationStream(); + } + + void _startLocationStream() { + _positionStreamSubscription = Geolocator.getPositionStream( + 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); + } + } + }, onError: (error) { + debugPrint("Erro no GPS: $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); + } + _currentSpeed = speed >= 0 ? speed : 0; + _routePoints.add(newPoint); + _currentPosition = newPoint; + _updateMarkers(); // Agora só atualiza a seta seguidora + + 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 (_mapController != null) { + _mapController!.animateCamera(CameraUpdate.newLatLng(newPoint)); + } + } + + void _updateMarkers() { + // Remove APENAS o marcador da seta seguidora para evitar duplicidade com o ponto de partida fixo + _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, + )); + } + + 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); + } + + void _onMapTap(LatLng point) { + 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)); + } else if (_plannedEnd == null) { + _plannedEnd = point; + _markers.add(Marker(markerId: const MarkerId('planned_end'), position: point, icon: _finishIcon ?? BitmapDescriptor.defaultMarker, zIndex: 5)); + _polylines.add(Polyline(polylineId: const PolylineId('planned_route'), points: [_plannedStart!, _plannedEnd!], color: Colors.white.withOpacity(0.1), width: 2, zIndex: 1)); + } 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)); + } + }); + } + + 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(); // Reposiciona a seta no início + }); + + _simulationTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) { + double latStep = 0.000025; + double lngStep = 0.000025; + + 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)); + } + + LatLng nextPoint = LatLng(_currentPosition.latitude + latStep, _currentPosition.longitude + lngStep); + _updatePosition(nextPoint, 3.5 + Random().nextDouble() * 0.5); + }); + } } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Mapa da Corrida'), - foregroundColor: Colors.white, - backgroundColor: Colors.black, // or your AppColors.background - elevation: 0, - ), - body: GoogleMap( - onMapCreated: _onMapCreated, - initialCameraPosition: CameraPosition(target: _center, zoom: 13.0), - ), + backgroundColor: AppColors.background, + appBar: AppBar(title: Text(_isPlanningMode ? 'PLANEJAR ROTA' : 'CORRIDA VIVA', 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))]), + 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("RITMO", "${(_currentSpeed * 3.6).toStringAsFixed(1)}", "KM/H"), Container(width: 1, height: 35, color: Colors.white10), _buildStat("TRAJETO", (_totalDistance / 1000).toStringAsFixed(2), "KM")]))), 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("Toque para definir Início e Fim", textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold))))]), + floatingActionButton: FloatingActionButton.extended(onPressed: _toggleSimulation, label: Text(_isSimulating ? "PARAR" : "INICIAR CORRIDA", 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), + 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))])]); + } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 75f0199..0f4ecc4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,9 @@ import FlutterMacOS import Foundation import flutter_blue_plus_darwin +import geolocator_apple func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 0655400..047adf5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" csslib: dependency: transitive description: @@ -97,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -184,6 +200,54 @@ packages: description: flutter source: sdk version: "0.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: f4efb8d3c4cdcad2e226af9661eb1a0dd38c71a9494b22526f9da80ab79520e5 + url: "https://pub.dev" + source: hosted + version: "10.1.1" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.dev" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" google_maps: dependency: transitive description: @@ -533,6 +597,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5b70c35..48359f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=3.10.7 <4.0.0' + sdk: ^3.10.7 dependencies: flutter: @@ -14,7 +14,8 @@ dependencies: flutter_map: ^6.1.0 latlong2: ^0.9.1 google_maps_flutter: ^2.14.2 - flutter_blue_plus: ^1.31.11 + geolocator: ^10.1.0 + flutter_blue_plus: ^1.31.0 permission_handler: ^11.3.1 dev_dependencies: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 48de52b..5be7b60 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0e69e40..b949ced 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + geolocator_windows permission_handler_windows )