rework do design, mapa, e adicao de uma tela provisoria sobre a conexao bluetooth que esta a funcionar (placeholder para oque o fabio fez ate aquilo estar tudo certo)

This commit is contained in:
2026-03-04 16:15:48 +00:00
parent bbd6185401
commit 666f06ab6a
10 changed files with 1093 additions and 256 deletions

View File

@@ -3,5 +3,7 @@
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -1,5 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required for Bluetooth -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
@@ -18,10 +27,6 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
@@ -31,21 +36,13 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- TODO: Replace with your actual Google Maps API Key -->
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCk84rxmF044cxKLABf55rEKHDqOcyoV5k"/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>

View File

@@ -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<RunningScreen>
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<double>(
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<RunningScreen>
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<RunningScreen>
@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<RunningScreen>
),
),
// 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<Color>(
AppColors.white,
),
minHeight: 8,
backgroundColor: Colors.white.withOpacity(0.1),
valueColor: const AlwaysStoppedAnimation<Color>(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<RunningScreen>
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<RunningScreen>
);
}
/// 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<RunningScreen>
),
);
}
/// 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,
);

View File

@@ -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<BluetoothConnectionScreen> createState() => _BluetoothConnectionScreenState();
}
class _BluetoothConnectionScreenState extends State<BluetoothConnectionScreen> {
List<ScanResult> _scanResults = [];
bool _isScanning = false;
BluetoothDevice? _connectedDevice; // Track connected device
late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
late StreamSubscription<bool> _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<void> _requestPermissionsAndStartScan() async {
if (Platform.isAndroid) {
Map<Permission, PermissionStatus> 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<void> _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<void> _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<void> _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<void> _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<Color>(AppColors.coral),
minHeight: 2,
),
),
],
),
);
}
}

View File

@@ -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<GoogleMapScreen> {
late GoogleMapController mapController;
GoogleMapController? _mapController;
StreamSubscription<Position>? _positionStreamSubscription;
Timer? _simulationTimer;
final List<LatLng> _routePoints = [];
final Set<Polyline> _polylines = {};
final Set<Marker> _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<void> _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<BitmapDescriptor> _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<BitmapDescriptor> _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<void> _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<LatLng> points) {
if (points.length < 2) return 0;
LatLng p1 = points[points.length - 2];
LatLng p2 = points.last;
return Geolocator.bearingBetween(p1.latitude, p1.longitude, p2.latitude, p2.longitude);
}
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))])]);
}
}

View File

@@ -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"))
}

View File

@@ -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"

View File

@@ -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:

View File

@@ -6,6 +6,12 @@
#include "generated_plugin_registrant.h"
#include <geolocator_windows/geolocator_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
}

View File

@@ -3,6 +3,8 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
geolocator_windows
permission_handler_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST