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 b099f59..a40a803 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,14 @@ - + + + + + + + + + + @@ -18,10 +27,6 @@ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> - - - - diff --git a/lib/main.dart b/lib/main.dart index 859eeb5..275b9a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'constants/app_colors.dart'; import 'screens/google_map_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()); } @@ -33,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, @@ -75,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, + ), ), ], ), @@ -95,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, + ), + ), + ), + ], + ), ), ), ), @@ -210,105 +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: () { - // Exibe uma mensagem rápida quando clicado. - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Bluetooth clicado!'), - duration: Duration(seconds: 1), - ), - ); - }, - 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, - child: CircleAvatar( + ? 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 0d20c1e..1082d89 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,26 +15,234 @@ class GoogleMapScreen extends StatefulWidget { } class _GoogleMapScreenState extends State { - late GoogleMapController mapController; + GoogleMapController? _mapController; + StreamSubscription? _positionStreamSubscription; + Timer? _simulationTimer; + + final List _routePoints = []; + final Set _polylines = {}; + final Set _markers = {}; + + LatLng? _plannedStart; + LatLng? _plannedEnd; + bool _isPlanningMode = false; - final LatLng _center = const LatLng(38.7223, -9.1393); // Lisbon coordinates + double _currentSpeed = 0.0; + double _totalDistance = 0.0; + LatLng _currentPosition = const LatLng(38.7223, -9.1393); + bool _isLoading = true; + bool _isSimulating = false; - void _onMapCreated(GoogleMapController controller) { - mapController = controller; + BitmapDescriptor? _startIcon; + BitmapDescriptor? _arrowIcon; + BitmapDescriptor? _finishIcon; + + @override + void initState() { + super.initState(); + _setupIconsAndTracking(); + } + + Future _setupIconsAndTracking() async { + _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(); + 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(); + _currentPosition = LatLng(position.latitude, position.longitude); + setState(() => _isLoading = false); + _startLocationStream(); + } + + void _startLocationStream() { + _positionStreamSubscription = Geolocator.getPositionStream(locationSettings: const LocationSettings(accuracy: LocationAccuracy.high, distanceFilter: 5)).listen((Position position) { + if (!_isSimulating) _updatePosition(LatLng(position.latitude, position.longitude), position.speed); + }); + } + + void _updatePosition(LatLng newPoint, double speed) { + setState(() { + if (_routePoints.isNotEmpty) { + _totalDistance += Geolocator.distanceBetween(_currentPosition.latitude, _currentPosition.longitude, newPoint.latitude, newPoint.longitude); + } + _currentSpeed = speed; + _routePoints.add(newPoint); + _currentPosition = newPoint; + + // Update only the dynamic follower arrow + _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, + )); + + 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.lightBlueAccent.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, + )); + } + }); + _mapController?.animateCamera(CameraUpdate.newLatLng(newPoint)); + } + + 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; + + // Start from planned start OR current real location + _currentPosition = _plannedStart ?? _currentPosition; + + // Initialize routePoints with the start so the arrow appears immediately at the start point + _routePoints.add(_currentPosition); + + // Update only the follower arrow. Planned markers are already in the set. + _markers.removeWhere((m) => m.markerId.value == 'follower'); + _markers.add(Marker( + markerId: const MarkerId('follower'), + position: _currentPosition, + flat: true, + anchor: const Offset(0.5, 0.5), + icon: _arrowIcon ?? BitmapDescriptor.defaultMarker, + zIndex: 12, + )); + }); + + _simulationTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + double latStep = 0.00025; + double lngStep = 0.00025; + if (_plannedEnd != null) { + double dist = Geolocator.distanceBetween(_currentPosition.latitude, _currentPosition.longitude, _plannedEnd!.latitude, _plannedEnd!.longitude); + if (dist < 12) { + _updatePosition(_plannedEnd!, 0.0); + _toggleSimulation(); + return; + } + latStep = (_plannedEnd!.latitude - _currentPosition.latitude) / (max(dist / 8, 1)); + lngStep = (_plannedEnd!.longitude - _currentPosition.longitude) / (max(dist / 8, 1)); + } + LatLng nextPoint = LatLng(_currentPosition.latitude + latStep, _currentPosition.longitude + lngStep); + _updatePosition(nextPoint, 3.5 + Random().nextDouble()); + }); + } } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Mapa da Corrida'), - 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 cccf817..0f4ecc4 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ 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 e2cb40b..047adf5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -9,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" + url: "https://pub.dev" + source: hosted + version: "0.8.3" boolean_selector: dependency: transitive description: @@ -21,10 +37,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: @@ -41,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: @@ -57,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" fake_async: dependency: transitive description: @@ -65,11 +97,75 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + 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 source: sdk version: "0.0.0" + flutter_blue_plus: + dependency: "direct main" + description: + name: flutter_blue_plus + sha256: "69a8c87c11fc792e8cf0f997d275484fbdb5143ac9f0ac4d424429700cb4e0ed" + url: "https://pub.dev" + source: hosted + version: "1.36.8" + flutter_blue_plus_android: + dependency: transitive + description: + name: flutter_blue_plus_android + sha256: "6f7fe7e69659c30af164a53730707edc16aa4d959e4c208f547b893d940f853d" + url: "https://pub.dev" + source: hosted + version: "7.0.4" + flutter_blue_plus_darwin: + dependency: transitive + description: + name: flutter_blue_plus_darwin + sha256: "682982862c1d964f4d54a3fb5fccc9e59a066422b93b7e22079aeecd9c0d38f8" + url: "https://pub.dev" + source: hosted + version: "7.0.3" + flutter_blue_plus_linux: + dependency: transitive + description: + name: flutter_blue_plus_linux + sha256: "56b0c45edd0a2eec8f85bd97a26ac3cd09447e10d0094fed55587bf0592e3347" + url: "https://pub.dev" + source: hosted + version: "7.0.3" + flutter_blue_plus_platform_interface: + dependency: transitive + description: + name: flutter_blue_plus_platform_interface + sha256: "84fbd180c50a40c92482f273a92069960805ce324e3673ad29c41d2faaa7c5c2" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter_blue_plus_web: + dependency: transitive + description: + name: flutter_blue_plus_web + sha256: a1aceee753d171d24c0e0cdadb37895b5e9124862721f25f60bb758e20b72c99 + url: "https://pub.dev" + source: hosted + version: "7.0.2" flutter_lints: dependency: "direct dev" description: @@ -104,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: @@ -244,18 +388,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + 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: @@ -280,6 +424,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" plugin_platform_interface: dependency: transitive description: @@ -304,6 +504,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" sanitize_html: dependency: transitive description: @@ -369,10 +577,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" typed_data: dependency: transitive description: @@ -389,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: @@ -421,6 +637,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" sdks: dart: ">=3.10.7 <4.0.0" flutter: ">=3.38.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3b223fd..48359f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,9 @@ dependencies: flutter_map: ^6.1.0 latlong2: ^0.9.1 google_maps_flutter: ^2.14.2 + geolocator: ^10.1.0 + flutter_blue_plus: ^1.31.0 + permission_handler: ^11.3.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..5be7b60 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +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 b93c4c3..b949ced 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + geolocator_windows + permission_handler_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST