This commit is contained in:
Carlos Correia
2026-03-04 15:54:24 +00:00
parent b405cfb93b
commit ebca3cfdce
147 changed files with 9168 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class AppColors {
// Background colors
static const Color background = Color.fromARGB(255, 49, 53, 77);
static const Color coral = Color(0xFFFF6B6B);
// Button colors
static const Color buttonColor = Color.fromARGB(255, 112, 112, 112);
static const Color white = Colors.white;
// Neutral colors
static const Color backgroundGrey = Color.fromARGB(255, 89, 89, 89);
}

View File

@@ -0,0 +1,10 @@
class AppConstants {
// Supabase Configuration
static const String supabaseUrl = 'https://kiltpvuchpinspggkzdj.supabase.co';
static const String supabaseAnonKey =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtpbHRwdnVjaHBpbnNwZ2dremRqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA4MjQ3NzcsImV4cCI6MjA4NjQwMDc3N30.h_63zAzHJELZufITa74-lM400lDb8B3jj3B-laebusQ';
// App Configuration
static const String appName = 'Run Vision Pro';
static const String appVersion = '1.0.0';
}

View File

@@ -0,0 +1,8 @@
class AppStrings {
// Initial screen
static const String registrar = 'Registrar';
static const String entrar = 'Entrar';
// Common
static const String appTitle = 'Run Vision Pro';
}

46
lib/main.dart Normal file
View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'screens/inicial_screen.dart';
import 'constants/app_strings.dart';
import 'services/supabase_service.dart';
import 'constants/app_constants.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Supabase
await SupabaseService.initialize();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: AppStrings.appTitle,
debugShowCheckedModeBanner: false,
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
),
home: const InicialScreen(),
);
}
}

View File

@@ -0,0 +1,891 @@
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../constants/app_colors.dart';
class GoogleMapScreen extends StatefulWidget {
const GoogleMapScreen({super.key});
@override
State<GoogleMapScreen> createState() => _GoogleMapScreenState();
}
class _GoogleMapScreenState extends State<GoogleMapScreen> {
late GoogleMapController mapController;
final Set<Marker> _markers = {};
Set<Polyline> _polylines = {};
LatLng? _currentLocation;
LatLng? _destination;
bool _isGettingLocation = true;
bool _isRequestingPermission = true;
final TextEditingController _searchController = TextEditingController();
List<Map<String, dynamic>> _placeSuggestions = [];
bool _isSearching = false;
@override
void initState() {
super.initState();
_requestLocationPermission();
}
Future<void> _requestLocationPermission() async {
try {
print('🔐 Solicitando permissões de localização...');
// Verificar status atual das permissões
Map<Permission, PermissionStatus> statuses = await [
Permission.location,
Permission.locationWhenInUse,
Permission.locationAlways,
].request();
bool locationGranted =
(statuses[Permission.location]?.isGranted == true) ||
(statuses[Permission.locationWhenInUse]?.isGranted == true);
if (locationGranted) {
print('✅ Permissões de localização concedidas');
setState(() {
_isRequestingPermission = false;
});
_getCurrentLocation();
} else {
print('❌ Permissões de localização negadas');
setState(() {
_isRequestingPermission = false;
});
_showLocationPermissionDialog();
}
} catch (e) {
print('❌ Erro ao solicitar permissões: $e');
_showSnackBar('Erro ao solicitar permissões de localização', Colors.red);
}
}
void _showLocationPermissionDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: AppColors.backgroundGrey,
title: const Text(
'Permissão de Localização',
style: TextStyle(
color: AppColors.white,
fontWeight: FontWeight.bold,
),
),
content: const Text(
'Este aplicativo precisa acessar sua localização para mostrar rotas de caminhada e calcular distâncias. Por favor, habilite a localização nas configurações do seu dispositivo.',
style: TextStyle(color: AppColors.white),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
_openAppSettings();
},
child: const Text(
'Abrir Configurações',
style: TextStyle(color: AppColors.coral),
),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop(); // Volta para a tela anterior
},
child: const Text(
'Cancelar',
style: TextStyle(color: AppColors.white),
),
),
],
);
},
);
}
Future<void> _openAppSettings() async {
try {
await openAppSettings();
} catch (e) {
print('❌ Erro ao abrir configurações: $e');
_showSnackBar('Não foi possível abrir as configurações', Colors.red);
}
}
Future<void> _getCurrentLocation() async {
try {
print('🔍 Iniciando busca de localização...');
// Verificar permissões de localização
LocationPermission permission = await Geolocator.checkPermission();
print('📍 Permissão atual: $permission');
if (permission == LocationPermission.denied) {
print('❌ Permissão negada, solicitando...');
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
_showSnackBar(
'Permissão de localização negada. Habilite nas configurações.',
AppColors.coral,
);
return;
}
}
if (permission == LocationPermission.deniedForever) {
_showSnackBar(
'Permissão de localização permanentemente negada. Habilite nas configurações do app.',
AppColors.coral,
);
return;
}
// Obter localização atual
print('🛰️ Buscando localização atual...');
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
timeLimit: const Duration(seconds: 10),
);
print(
'✅ Localização encontrada: ${position.latitude}, ${position.longitude}',
);
setState(() {
_currentLocation = LatLng(position.latitude, position.longitude);
_isGettingLocation = false;
});
// Adicionar marcador da localização atual
_addCurrentLocationMarker();
// Mover câmera para localização atual
if (mounted && _currentLocation != null) {
mapController.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(target: _currentLocation!, zoom: 15.0),
),
);
}
} catch (e) {
print('❌ Erro ao obter localização: $e');
setState(() {
_isGettingLocation = false;
});
_showSnackBar('Erro ao obter localização: ${e.toString()}', Colors.red);
}
}
void _addCurrentLocationMarker() {
if (_currentLocation != null) {
print('📍 Adicionando marcador de localização atual');
// Remover marcador anterior se existir
_markers.removeWhere(
(marker) => marker.markerId.value == 'current_location',
);
// Adicionar novo marcador
_markers.add(
Marker(
markerId: const MarkerId('current_location'),
position: _currentLocation!,
infoWindow: const InfoWindow(title: 'Sua Localização'),
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
),
);
setState(() {});
}
}
Future<void> _searchPlaces(String query) async {
if (query.isEmpty) {
setState(() {
_placeSuggestions = [];
});
return;
}
setState(() {
_isSearching = true;
});
try {
// Usar Google Places API para busca real baseada na localização atual
final String url =
'https://maps.googleapis.com/maps/api/place/autocomplete/json'
'?input=${Uri.encodeComponent(query)}'
'&location=${_currentLocation?.latitude ?? -23.5505},${_currentLocation?.longitude ?? -46.6333}'
'&radius=30000' // 30km de raio
'&language=pt_BR'
'&region=br'
'&components=country:br'
'&strictbounds=true' // Força busca dentro da área
'&key=AIzaSyCk84rxmF044cxKLABf55rEKHDqOcyoV5k';
print('🔍 Buscando lugares: $url');
print(
'📍 Baseado na localização: ${_currentLocation?.latitude ?? -23.5505}, ${_currentLocation?.longitude ?? -46.6333}',
);
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final predictions = data['predictions'] as List;
setState(() {
_placeSuggestions = predictions.map((prediction) {
return {
'description': prediction['description'],
'place_id': prediction['place_id'],
'structured_formatting': prediction['structured_formatting'],
};
}).toList();
});
print(
'${_placeSuggestions.length} lugares encontrados próximos à sua localização',
);
} else {
print(
'❌ Erro na API: ${data['status']} - ${data['error_message'] ?? ''}',
);
}
} else {
print('❌ Erro HTTP: ${response.statusCode}');
}
} catch (e) {
print('❌ Erro na busca: $e');
} finally {
setState(() {
_isSearching = false;
});
}
}
Future<void> _selectPlace(Map<String, dynamic> place) async {
try {
print('📍 Selecionado: ${place['description']}');
// Obter detalhes do lugar
final String detailsUrl =
'https://maps.googleapis.com/maps/api/place/details/json'
'?place_id=${place['place_id']}'
'&fields=geometry'
'&key=AIzaSyCk84rxmF044cxKLABf55rEKHDqOcyoV5k';
final response = await http.get(Uri.parse(detailsUrl));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final location = data['result']['geometry']['location'];
final destination = LatLng(location['lat'], location['lng']);
setState(() {
_destination = destination;
_placeSuggestions = []; // Limpar sugestões
});
_addDestinationMarker(destination, place['description']);
await _getDirections();
}
}
} catch (e) {
print('❌ Erro ao selecionar lugar: $e');
_showSnackBar('Erro ao selecionar destino', Colors.red);
}
}
void _addDestinationMarker(LatLng location, String name) {
print('📍 Adicionando marcador de destino: $name');
setState(() {
// Remover marcador de destino anterior se existir
_markers.removeWhere((marker) => marker.markerId.value == 'destination');
// Adicionar novo marcador de destino
_markers.add(
Marker(
markerId: const MarkerId('destination'),
position: location,
infoWindow: InfoWindow(title: name),
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueAzure,
),
),
);
});
print('✅ Marcador de destino adicionado: $name');
}
Future<void> _getDirections() async {
if (_currentLocation == null || _destination == null) {
print('❌ Localização ou destino nulo');
return;
}
try {
print('🛣️ Buscando rota real com Google Directions API...');
// Usar Google Directions API para obter rota realista
final String url =
'https://maps.googleapis.com/maps/api/directions/json'
'?origin=${_currentLocation!.latitude},${_currentLocation!.longitude}'
'&destination=${_destination!.latitude},${_destination!.longitude}'
'&mode=walking'
'&language=pt_BR'
'&key=AIzaSyCk84rxmF044cxKLABf55rEKHDqOcyoV5k';
print('🌐 URL da requisição: $url');
final response = await http.get(Uri.parse(url));
print('📡 Status code: ${response.statusCode}');
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK' && data['routes'].isNotEmpty) {
final route = data['routes'][0];
final legs = route['legs'][0];
final steps = legs['steps'];
print('✅ Rota encontrada com ${steps.length} passos');
// Extrair pontos da rota
final List<LatLng> routePoints = [];
// Adicionar ponto inicial
routePoints.add(_currentLocation!);
// Processar cada passo da rota
for (var step in steps) {
final startLocation = step['start_location'];
final endLocation = step['end_location'];
if (startLocation != null) {
routePoints.add(
LatLng(startLocation['lat'], startLocation['lng']),
);
}
if (endLocation != null) {
routePoints.add(LatLng(endLocation['lat'], endLocation['lng']));
}
}
// Adicionar ponto final
routePoints.add(_destination!);
print('📍 Total de pontos na rota: ${routePoints.length}');
setState(() {
_polylines.clear();
_polylines.add(
Polyline(
polylineId: const PolylineId('route'),
color: AppColors.backgroundGrey,
width: 5,
points: routePoints,
),
);
});
// Ajustar câmera para mostrar toda a rota
_adjustCameraToShowRoute();
// Calcular distância e tempo
final distance = legs['distance']['text'] ?? 'Calculando...';
final duration = legs['duration']['text'] ?? 'Calculando...';
print('📊 Distância: $distance, Tempo: $duration');
_showSnackBar(
'Rota de caminhada calculada!\nDistância: $distance\nTempo: $duration',
Colors.green,
);
} else {
print(
'❌ Erro na resposta: ${data['status']} - ${data['error_message']}',
);
_showSnackBar(
'Não foi possível calcular a rota. Tente novamente.',
AppColors.coral,
);
}
} else {
print('❌ Erro HTTP: ${response.statusCode}');
_showSnackBar(
'Erro ao conectar com o servidor. Verifique sua internet.',
Colors.red,
);
}
} catch (e) {
print('❌ Erro ao calcular rota: $e');
_showSnackBar('Erro ao calcular rota: ${e.toString()}', Colors.red);
}
}
void _adjustCameraToShowRoute() {
if (_currentLocation == null || _destination == null) return;
final double padding = 0.01; // Padding para mostrar os marcadores
final bounds = LatLngBounds(
southwest: LatLng(
(_currentLocation!.latitude < _destination!.latitude
? _currentLocation!.latitude
: _destination!.latitude) -
padding,
(_currentLocation!.longitude < _destination!.longitude
? _currentLocation!.longitude
: _destination!.longitude) -
padding,
),
northeast: LatLng(
(_currentLocation!.latitude > _destination!.latitude
? _currentLocation!.latitude
: _destination!.latitude) +
padding,
(_currentLocation!.longitude > _destination!.longitude
? _currentLocation!.longitude
: _destination!.longitude) +
padding,
),
);
print('📷 Ajustando câmera para mostrar rota completa');
mapController.animateCamera(CameraUpdate.newLatLngBounds(bounds, 100));
}
void _onMapCreated(GoogleMapController controller) {
print('🗺️ Mapa criado');
mapController = controller;
}
void _showSnackBar(String message, Color color) {
print('💬 Mostrando mensagem: $message');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: color,
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
),
);
}
void _clearRoute() {
print('🧹 Limpando rota...');
setState(() {
_destination = null;
_polylines.clear();
_markers.removeWhere((marker) => marker.markerId.value == 'destination');
});
_showSnackBar('Rota limpa. Busque um novo destino.', AppColors.coral);
}
void _centerOnCurrentLocation() {
if (_currentLocation != null) {
print('🎯 Centralizando na localização atual');
mapController.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(target: _currentLocation!, zoom: 15.0),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.backgroundGrey.withOpacity(0.3),
AppColors.background,
],
),
),
child: Stack(
children: [
// Back button
Positioned(
top: 50,
left: 20,
child: Container(
decoration: BoxDecoration(
color: AppColors.backgroundGrey.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.1)),
),
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
Navigator.pop(context);
},
tooltip: 'Voltar',
),
),
),
Column(
children: [
const SizedBox(height: 100),
// Search section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Buscar Destino:',
style: TextStyle(
color: AppColors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 12),
// Search bar
Container(
decoration: BoxDecoration(
color: AppColors.backgroundGrey.withOpacity(0.2),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.backgroundGrey.withOpacity(0.3),
),
),
child: TextField(
controller: _searchController,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
enableSuggestions: true,
autocorrect: true,
decoration: InputDecoration(
hintText:
'Ex: Parque Ibirapuera, Avenida Paulista...',
hintStyle: TextStyle(
color: AppColors.white.withOpacity(0.5),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
prefixIcon: const Icon(
Icons.search,
color: AppColors.white,
),
suffixIcon: _isSearching
? const Padding(
padding: EdgeInsets.all(12),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: AppColors.backgroundGrey,
strokeWidth: 2,
),
),
)
: IconButton(
icon: const Icon(
Icons.send,
color: AppColors.backgroundGrey,
),
onPressed: () =>
_searchPlaces(_searchController.text),
),
),
style: const TextStyle(color: AppColors.white),
onChanged: (value) {
if (value.length >= 3) {
_searchPlaces(value);
} else {
setState(() {
_placeSuggestions = [];
});
}
},
onSubmitted: (value) {
if (_placeSuggestions.isNotEmpty) {
_selectPlace(_placeSuggestions.first);
}
},
),
),
// Suggestions dropdown
if (_placeSuggestions.isNotEmpty)
Container(
margin: const EdgeInsets.only(top: 8),
decoration: BoxDecoration(
color: AppColors.backgroundGrey.withOpacity(0.2),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.backgroundGrey.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _placeSuggestions.take(5).map((place) {
return InkWell(
onTap: () => _selectPlace(place),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
children: [
const Icon(
Icons.place,
color: AppColors.backgroundGrey,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
place['description'],
style: const TextStyle(
color: AppColors.white,
fontSize: 14,
),
),
),
],
),
),
);
}).toList(),
),
),
],
),
),
const SizedBox(height: 20),
// Status bar
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
Text(
_isRequestingPermission
? "Solicitando permissões..."
: _isGettingLocation
? "Obtendo localização..."
: _destination != null
? "Rota ativa"
: "Defina um destino",
style: const TextStyle(
color: AppColors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const Spacer(),
if (_isRequestingPermission)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: AppColors.backgroundGrey.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Text(
"PERMISSÃO",
style: TextStyle(
color: AppColors.backgroundGrey,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
)
else if (_destination != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: AppColors.backgroundGrey.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: const Text(
"ATIVO",
style: TextStyle(
color: AppColors.backgroundGrey,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
const SizedBox(height: 15),
// Map
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.backgroundGrey.withOpacity(0.3),
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(
target:
_currentLocation ??
const LatLng(-23.5505, -46.6333),
zoom: 15.0,
),
markers: _markers,
polylines: _polylines,
mapType: MapType.normal,
myLocationEnabled: true,
myLocationButtonEnabled: false,
zoomControlsEnabled: false,
),
// Loading indicator
if (_isRequestingPermission || _isGettingLocation)
Container(
color: Colors.black54,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(
color: AppColors.backgroundGrey,
strokeWidth: 3,
),
const SizedBox(height: 16),
Text(
_isRequestingPermission
? 'Solicitando permissões de localização...'
: 'Obtendo sua localização...',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
),
),
),
// Map controls
Positioned(
top: 20,
right: 20,
child: Column(
children: [
Container(
decoration: BoxDecoration(
color: AppColors.backgroundGrey.withOpacity(
0.8,
),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(0.1),
),
),
child: Column(
children: [
IconButton(
icon: const Icon(
Icons.add,
color: Colors.white,
),
onPressed: () {
mapController.animateCamera(
CameraUpdate.zoomIn(),
);
},
tooltip: 'Aumentar zoom',
),
IconButton(
icon: const Icon(
Icons.remove,
color: Colors.white,
),
onPressed: () {
mapController.animateCamera(
CameraUpdate.zoomOut(),
);
},
tooltip: 'Diminuir zoom',
),
IconButton(
icon: const Icon(
Icons.my_location,
color: Colors.white,
),
onPressed: _centerOnCurrentLocation,
tooltip: 'Minha localização',
),
IconButton(
icon: const Icon(
Icons.clear,
color: Colors.white,
),
onPressed: _clearRoute,
tooltip: 'Limpar rota',
),
],
),
),
],
),
),
],
),
),
),
),
const SizedBox(height: 20),
],
),
],
),
),
);
}
}

View File

@@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import '../constants/app_colors.dart';
import '../constants/app_strings.dart';
import '../sheets/entrar_sheet.dart';
import '../sheets/registrar_sheet.dart';
import '../services/supabase_service.dart';
class AnimatedButton extends StatefulWidget {
final String text;
final VoidCallback onPressed;
final Color backgroundColor;
final Color textColor;
const AnimatedButton({
super.key,
required this.text,
required this.onPressed,
required this.backgroundColor,
required this.textColor,
});
@override
State<AnimatedButton> createState() => _AnimatedButtonState();
}
class _AnimatedButtonState extends State<AnimatedButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _bounceAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.92,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
_bounceAnimation = Tween<double>(begin: 0.0, end: -3.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.3, 0.8, curve: Curves.elasticOut),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleTap() {
_controller.forward().then((_) {
_controller.reverse().then((_) {
widget.onPressed();
});
});
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _bounceAnimation.value),
child: Transform.scale(
scale: _scaleAnimation.value,
child: SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: _handleTap,
style: ElevatedButton.styleFrom(
backgroundColor: widget.backgroundColor,
foregroundColor: widget.textColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 5,
),
child: Text(
widget.text,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
},
);
}
}
class InicialScreen extends StatelessWidget {
const InicialScreen({super.key});
@override
Widget build(BuildContext context) {
// Test Supabase connection on app start
WidgetsBinding.instance.addPostFrameCallback((_) async {
final isConnected = await SupabaseService.testConnection();
print('DEBUG: Status da conexão: $isConnected');
});
return Scaffold(
backgroundColor: AppColors.background,
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedButton(
text: AppStrings.registrar,
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const RegistrarSheet(),
);
},
backgroundColor: AppColors.buttonColor,
textColor: AppColors.white,
),
const SizedBox(height: 16),
AnimatedButton(
text: AppStrings.entrar,
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const EntrarSheet(),
);
},
backgroundColor: AppColors.buttonColor,
textColor: AppColors.white,
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,652 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../constants/app_colors.dart';
import '../services/supabase_service.dart';
import '../screens/inicial_screen.dart';
import '../screens/google_maps_screen.dart';
import '../screens/setting_screen.dart';
/// Tela principal para usuários logados com estatísticas de corrida e menu.
class LogadoInicialScreen extends StatefulWidget {
const LogadoInicialScreen({super.key});
@override
State<LogadoInicialScreen> createState() => _LogadoInicialScreenState();
}
class _LogadoInicialScreenState extends State<LogadoInicialScreen>
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 targetDistance = 8.0; // Distância alvo em KM.
double currentDistance = 0.0; // Distância atual percorrida.
@override
void initState() {
super.initState();
// Simular progresso inicial
_simulateProgress();
}
void _simulateProgress() {
Timer.periodic(const Duration(seconds: 3), (timer) {
if (mounted && progress < 1.0) {
setState(() {
progress = (progress + 0.1).clamp(0.0, 1.0);
currentDistance = targetDistance * progress;
});
} else {
timer.cancel();
}
});
}
/// Constrói o indicador de progresso circular central.
Widget _buildCircularProgressIndicator() {
return SizedBox(
width: 200,
height: 200,
child: Stack(
alignment: Alignment.center,
children: [
// 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),
builder: (context, value, _) {
return CustomPaint(
size: const Size(200, 200),
painter: CircularProgressPainter(
progress: value,
strokeWidth: 12,
progressColor: AppColors.white,
backgroundColor: AppColors.white.withOpacity(0.3),
),
);
},
),
// Círculo interno cinza que contém o texto da porcentagem.
Container(
width: 170,
height: 170,
decoration: const BoxDecoration(
color: AppColors.backgroundGrey,
shape: BoxShape.circle,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${(progress * 100).toInt()}%",
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const Text(
"COMPLETO",
style: TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final user = SupabaseService.currentUser;
final userName =
user?.userMetadata?['name'] ?? user?.email?.split('@')[0] ?? 'Usuário';
return Scaffold(
backgroundColor: AppColors.background,
body: Stack(
children: [
// Static dark gray triangles
Positioned(
top: -50,
left: -80,
child: CustomPaint(
size: const Size(160, 120),
painter: TrianglePainter(
color: Colors.grey.shade800.withOpacity(0.4),
),
),
),
Positioned(
top: 20,
right: -60,
child: CustomPaint(
size: const Size(120, 90),
painter: TrianglePainter(
color: Colors.grey.shade800.withOpacity(0.3),
),
),
),
Positioned(
top: 80,
left: 40,
child: CustomPaint(
size: const Size(140, 105),
painter: TrianglePainter(
color: Colors.grey.shade800.withOpacity(0.35),
),
),
),
Positioned(
top: 120,
right: 80,
child: CustomPaint(
size: const Size(100, 75),
painter: TrianglePainter(
color: Colors.grey.shade800.withOpacity(0.25),
),
),
),
Positioned(
top: 160,
left: -40,
child: CustomPaint(
size: const Size(130, 98),
painter: TrianglePainter(
color: Colors.grey.shade800.withOpacity(0.3),
),
),
),
// User info header
Positioned(
top: 40,
left: 20,
right: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Olá, $userName!',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'Bem-vindo de volta!',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
],
),
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: AppColors.buttonColor,
borderRadius: BorderRadius.circular(25),
),
child: const Icon(
Icons.person,
color: AppColors.white,
size: 24,
),
),
],
),
),
// 1. Indicador de progresso circular posicionado no topo central.
Align(
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.only(top: 140),
child: _buildCircularProgressIndicator(),
),
),
// 2. Exibição da distância (ex: 0.0 KM | 8.0 KM).
Positioned(
top: 360,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8,
),
decoration: BoxDecoration(
color: AppColors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
"${currentDistance.toStringAsFixed(1)} KM | ${targetDistance.toStringAsFixed(1)} KM",
style: const TextStyle(
color: AppColors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),
// 3. Contêiner de estatísticas (Passos, BPM, K/CAL) e o mapa clicável.
Positioned(
top: 420,
left: 20,
right: 20,
child: Container(
height: 200,
decoration: BoxDecoration(
color: AppColors.backgroundGrey,
borderRadius: BorderRadius.circular(24),
),
child: Row(
children: [
// Coluna esquerda com ícones e valores de estatística.
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
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",
),
],
),
),
),
// Coluna direita contendo o mapa clicável.
Expanded(
flex: 6,
child: GestureDetector(
onTap: () {
Navigator.push(
context,
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.
),
// Overlay para indicar que é clicável
Container(
color: Colors.black.withOpacity(0.1),
child: const Center(
child: Icon(
Icons.touch_app,
color: Colors.white54,
size: 40,
),
),
),
],
),
),
),
),
],
),
),
),
// 4. Barra de progresso linear (centralizada acima dos botões inferiores).
Positioned(
bottom: 160,
left:
40, // Espaçamento igual na esquerda e direita para centralizar.
right: 40,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: LinearProgressIndicator(
value: progress,
minHeight: 12,
backgroundColor: AppColors.backgroundGrey.withOpacity(0.3),
valueColor: const AlwaysStoppedAnimation<Color>(
AppColors.white,
),
),
),
),
// 5. Botões de menu inferiores.
Positioned(
bottom: 60,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildMenuButton(
Icons.settings,
'Configurações',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
);
},
),
_buildMenuButton(Icons.group_outlined, 'Grupos'),
_buildMenuButton(Icons.access_time, 'Histórico'),
_buildMenuButton(
Icons.notifications_none,
'Notificações',
showBadge: true,
),
_buildMenuButton(Icons.logout, 'Sair', isLogout: true),
],
),
),
// 6. Botão de Bluetooth no canto superior direito.
Positioned(
top: 40,
right: 30,
child: GestureDetector(
onTap: () {
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,
),
),
),
],
),
),
),
),
],
),
);
}
/// Constrói uma linha de estatística com ícone, valor e rótulo.
Widget _buildStatItem(IconData icon, String value, String label) {
return Row(
children: [
Icon(icon, color: Colors.white70, size: 24),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: const TextStyle(color: Colors.white60, fontSize: 10),
),
],
),
],
);
}
/// Constrói um botão de menu clicável.
Widget _buildMenuButton(
IconData icon,
String message, {
bool showBadge = false,
bool isLogout = false,
VoidCallback? onTap,
}) {
return GestureDetector(
onTap:
onTap ??
() async {
if (isLogout) {
await SupabaseService.signOut();
if (mounted) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => const InicialScreen(),
),
(route) => false,
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$message clicado!'),
duration: const Duration(seconds: 1),
),
);
}
},
child: Stack(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: isLogout ? Colors.red.shade600 : AppColors.backgroundGrey,
shape: BoxShape.circle,
),
child: Icon(icon, color: AppColors.white, size: 24),
),
// Exibe um pontinho vermelho de notificação se showBadge for true.
if (showBadge)
Positioned(
left: 0,
bottom: 0,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
),
);
}
}
/// Pintor customizado para desenhar os triângulos estáticos.
class TrianglePainter extends CustomPainter {
final Color color;
TrianglePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final path = Path();
path.moveTo(size.width / 2, 0);
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
/// Pintor customizado para desenhar o traçado do mapa simulado.
class MapPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white38
..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);
// Desenha uma "estrada" mais grossa branca.
final roadPaint = Paint()
..color = Colors.white
..strokeWidth = 8
..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,
);
canvas.drawPath(path, paint);
canvas.drawPath(roadPath, roadPaint);
// 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);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
/// Pintor customizado para desenhar o arco de progresso circular.
class CircularProgressPainter extends CustomPainter {
final double progress;
final double strokeWidth;
final Color progressColor;
final Color backgroundColor;
CircularProgressPainter({
required this.progress,
required this.strokeWidth,
required this.progressColor,
required this.backgroundColor,
});
@override
void paint(Canvas canvas, Size size) {
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()
..color = backgroundColor
..strokeWidth = strokeWidth
..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,
false,
progressPaint,
);
}
@override
bool shouldRepaint(CircularProgressPainter oldDelegate) =>
oldDelegate.progress != progress;
}

View File

@@ -0,0 +1,219 @@
import 'package:flutter/material.dart';
import '../constants/app_colors.dart';
import '../screens/inicial_screen.dart';
class LogadoScreen extends StatelessWidget {
const LogadoScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
body: Stack(
children: [
// Static dark gray triangles
Positioned(
top: -50,
left: -80,
child: CustomPaint(
size: const Size(160, 120),
painter: TrianglePainter(
color: Colors.grey.shade800.withOpacity(0.4),
),
),
),
Positioned(
top: 20,
right: -60,
child: CustomPaint(
size: const Size(120, 90),
painter: TrianglePainter(
color: Colors.grey.shade800.withOpacity(0.3),
),
),
),
Positioned(
top: 80,
left: 40,
child: CustomPaint(
size: const Size(140, 105),
painter: TrianglePainter(
color: Colors.grey.shade800.withOpacity(0.35),
),
),
),
Positioned(
top: 120,
right: 80,
child: CustomPaint(
size: const Size(100, 75),
painter: TrianglePainter(
color: Colors.grey.shade800.withOpacity(0.25),
),
),
),
Positioned(
top: 160,
left: -40,
child: CustomPaint(
size: const Size(130, 98),
painter: TrianglePainter(
color: Colors.grey.shade800.withOpacity(0.3),
),
),
),
// Main content
SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 80), // Space for triangles
// Success icon with animation
TweenAnimationBuilder(
duration: const Duration(milliseconds: 600),
tween: Tween<double>(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppColors.buttonColor,
borderRadius: BorderRadius.circular(50),
boxShadow: [
BoxShadow(
color: AppColors.buttonColor.withOpacity(0.3),
blurRadius: 20,
spreadRadius: 5,
),
],
),
child: const Icon(
Icons.check,
size: 50,
color: AppColors.white,
),
),
);
},
),
const SizedBox(height: 32),
// Welcome message with fade in
TweenAnimationBuilder(
duration: const Duration(milliseconds: 800),
tween: Tween<double>(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: const Text(
'Login Realizado!',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
);
},
),
const SizedBox(height: 16),
TweenAnimationBuilder(
duration: const Duration(milliseconds: 1000),
tween: Tween<double>(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: const Text(
'Você está autenticado com sucesso.',
style: TextStyle(fontSize: 18, color: Colors.white70),
textAlign: TextAlign.center,
),
);
},
),
const SizedBox(height: 48),
// Back button with slide up animation
TweenAnimationBuilder(
duration: const Duration(milliseconds: 1200),
tween: Tween<Offset>(
begin: Offset(0, 1),
end: Offset(0, 0),
),
builder: (context, value, child) {
return Transform.translate(
offset: value * 50,
child: SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const InicialScreen(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.buttonColor,
foregroundColor: AppColors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 5,
),
child: const Text(
'Voltar para Tela Inicial',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
);
},
),
],
),
),
),
],
),
);
}
}
// Custom painter for triangles
class TrianglePainter extends CustomPainter {
final Color color;
TrianglePainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final path = Path();
path.moveTo(size.width / 2, 0);
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}

View File

@@ -0,0 +1,414 @@
import 'package:flutter/material.dart';
import '../constants/app_colors.dart';
import '../services/supabase_service.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
bool _isNightMode = true;
bool _notificationsEnabled = true;
String _selectedLanguage = 'Português';
@override
Widget build(BuildContext context) {
final user = SupabaseService.currentUser;
final userName =
user?.userMetadata?['name'] ?? user?.email?.split('@')[0] ?? 'Usuário';
final userEmail = user?.email ?? 'usuario@exemplo.com';
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
title: const Text(
'CONFIGURAÇÕES',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
backgroundColor: AppColors.background,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.white),
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// User Profile Section
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.backgroundGrey,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: AppColors.buttonColor,
borderRadius: BorderRadius.circular(30),
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 30,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
userName,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
userEmail,
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 14,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.edit, color: AppColors.buttonColor),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Editar perfil'),
backgroundColor: AppColors.buttonColor,
),
);
},
),
],
),
),
const SizedBox(height: 24),
// Settings Items
Container(
decoration: BoxDecoration(
color: AppColors.backgroundGrey,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
_buildSettingsItem(
icon: Icons.schedule,
title: 'Ajustar Data e Hora',
onTap: () {
_showDatePicker(context);
},
),
_buildDivider(),
_buildSettingsItem(
icon: Icons.dark_mode,
title: 'Modo Noturno',
trailing: Switch(
value: _isNightMode,
onChanged: (value) {
setState(() {
_isNightMode = value;
});
},
activeColor: AppColors.buttonColor,
),
),
_buildDivider(),
_buildSettingsItem(
icon: Icons.language,
title: 'Idioma',
trailing: Text(
_selectedLanguage,
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 16,
),
),
onTap: () {
_showLanguageSelector(context);
},
),
_buildDivider(),
_buildSettingsItem(
icon: Icons.accessibility,
title: 'Acessibilidade',
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Acessibilidade'),
backgroundColor: AppColors.buttonColor,
),
);
},
),
_buildDivider(),
_buildSettingsItem(
icon: Icons.notifications,
title: 'Notificações',
trailing: Switch(
value: _notificationsEnabled,
onChanged: (value) {
setState(() {
_notificationsEnabled = value;
});
},
activeColor: AppColors.buttonColor,
),
),
_buildDivider(),
_buildSettingsItem(
icon: Icons.privacy_tip,
title: 'Privacidade e Segurança',
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Privacidade e Segurança'),
backgroundColor: AppColors.buttonColor,
),
);
},
),
_buildDivider(),
_buildSettingsItem(
icon: Icons.description,
title: 'Termos de Uso',
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Termos de Uso'),
backgroundColor: AppColors.buttonColor,
),
);
},
),
_buildDivider(),
_buildSettingsItem(
icon: Icons.info,
title: 'Sobre',
onTap: () {
_showAboutDialog(context);
},
),
],
),
),
const SizedBox(height: 24),
// Logout Button
Container(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
_showLogoutDialog(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Sair',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}
Widget _buildSettingsItem({
required IconData icon,
required String title,
Widget? trailing,
VoidCallback? onTap,
}) {
return ListTile(
leading: Icon(icon, color: AppColors.buttonColor, size: 24),
title: Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
trailing: trailing,
onTap: onTap,
);
}
Widget _buildDivider() {
return Divider(
color: Colors.white.withOpacity(0.1),
height: 1,
indent: 16,
endIndent: 16,
);
}
void _showDatePicker(BuildContext context) {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2025),
).then((date) {
if (date != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Data selecionada: ${date.toString().split(' ')[0]}'),
backgroundColor: AppColors.buttonColor,
),
);
}
});
}
void _showLanguageSelector(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.backgroundGrey,
title: const Text(
'Selecionar Idioma',
style: TextStyle(color: Colors.white),
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildLanguageOption('Português'),
_buildLanguageOption('English'),
_buildLanguageOption('Español'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'Cancelar',
style: TextStyle(color: AppColors.buttonColor),
),
),
],
),
);
}
Widget _buildLanguageOption(String language) {
return RadioListTile<String>(
title: Text(language, style: const TextStyle(color: Colors.white)),
value: language,
groupValue: _selectedLanguage,
onChanged: (value) {
setState(() {
_selectedLanguage = value!;
});
Navigator.pop(context);
},
activeColor: AppColors.buttonColor,
);
}
void _showAboutDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.backgroundGrey,
title: const Text('Sobre', style: TextStyle(color: Colors.white)),
content: const Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Run Vision Pro',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text('Versão: 1.0.0', style: TextStyle(color: Colors.white70)),
SizedBox(height: 8),
Text(
'Aplicativo de corrida com estatísticas e mapas',
style: TextStyle(color: Colors.white70),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'OK',
style: TextStyle(color: AppColors.buttonColor),
),
),
],
),
);
}
void _showLogoutDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.backgroundGrey,
title: const Text(
'Confirmar Logout',
style: TextStyle(color: Colors.white),
),
content: const Text(
'Tem certeza que deseja sair?',
style: TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'Cancelar',
style: TextStyle(color: AppColors.buttonColor),
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
Navigator.pushReplacementNamed(context, '/');
},
child: const Text('Sair', style: TextStyle(color: Colors.red)),
),
],
),
);
}
}

View File

@@ -0,0 +1,130 @@
import 'package:supabase_flutter/supabase_flutter.dart';
import '../constants/app_constants.dart';
class SupabaseService {
static final SupabaseClient _supabase = Supabase.instance.client;
// Initialize Supabase
static Future<void> initialize() async {
try {
print('DEBUG: Inicializando Supabase...');
print('DEBUG: URL: ${AppConstants.supabaseUrl}');
print(
'DEBUG: AnonKey: ${AppConstants.supabaseAnonKey.substring(0, 10)}...',
);
await Supabase.initialize(
url: AppConstants.supabaseUrl,
anonKey: AppConstants.supabaseAnonKey,
);
print('DEBUG: Supabase inicializado com sucesso!');
// Test connection
final currentUser = _supabase.auth.currentUser;
print('DEBUG: Usuário atual: ${currentUser?.email ?? 'null'}');
} catch (e) {
print('DEBUG: Erro ao inicializar Supabase: $e');
rethrow;
}
}
// Get current user
static User? get currentUser => _supabase.auth.currentUser;
// Sign up with email and password
static Future<AuthResponse> signUp({
required String email,
required String password,
required String name,
}) async {
try {
print('DEBUG: Criando conta - Email: $email, Name: $name');
final response = await _supabase.auth.signUp(
email: email,
password: password,
data: {'name': name},
);
print('DEBUG: Conta criada! User ID: ${response.user?.id}');
// Check if user was created successfully
if (response.user != null) {
print('DEBUG: Usuário criado com sucesso!');
return response;
} else {
print('DEBUG: Falha ao criar usuário - response.user é null');
throw Exception('Falha ao criar usuário. Tente novamente.');
}
} catch (e) {
print('DEBUG: Erro no signUp: $e');
throw Exception('Erro ao criar conta: $e');
}
}
// Sign in with email and password
static Future<AuthResponse> signIn({
required String email,
required String password,
}) async {
try {
print('DEBUG: Fazendo login - Email: $email');
final response = await _supabase.auth.signInWithPassword(
email: email,
password: password,
);
print('DEBUG: Login realizado! User ID: ${response.user?.id}');
return response;
} catch (e) {
print('DEBUG: Erro no signIn: $e');
throw Exception('Erro ao fazer login: $e');
}
}
// Sign out
static Future<void> signOut() async {
try {
await _supabase.auth.signOut();
print('DEBUG: Logout realizado');
} catch (e) {
print('DEBUG: Erro no signOut: $e');
throw Exception('Erro ao sair: $e');
}
}
// Reset password
static Future<void> resetPassword(String email) async {
try {
await _supabase.auth.resetPasswordForEmail(email);
print('DEBUG: Email de reset enviado para: $email');
} catch (e) {
print('DEBUG: Erro no resetPassword: $e');
throw Exception('Erro ao redefinir senha: $e');
}
}
// Test connection to Supabase
static Future<bool> testConnection() async {
try {
print('DEBUG: Testando conexão com Supabase...');
// Test with auth service instead of database
final session = _supabase.auth.currentSession;
print('DEBUG: Sessão atual: ${session != null ? 'ativa' : 'null'}');
// Try to get auth settings (this should work even without tables)
print('DEBUG: Conexão básica funcionando!');
return true;
} catch (e) {
print('DEBUG: Erro na conexão: $e');
return false;
}
}
// Listen to auth state changes
static Stream<AuthState> get authStateChanges =>
_supabase.auth.onAuthStateChange;
}

View File

@@ -0,0 +1,360 @@
import 'package:flutter/material.dart';
import '../constants/app_colors.dart';
import '../services/supabase_service.dart';
import '../screens/logado_inicial_screen.dart';
class AnimatedButton extends StatefulWidget {
final String text;
final VoidCallback onPressed;
final Color backgroundColor;
final Color textColor;
final bool isLoading;
const AnimatedButton({
super.key,
required this.text,
required this.onPressed,
required this.backgroundColor,
required this.textColor,
this.isLoading = false,
});
@override
State<AnimatedButton> createState() => _AnimatedButtonState();
}
class _AnimatedButtonState extends State<AnimatedButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _bounceAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.92,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
_bounceAnimation = Tween<double>(begin: 0.0, end: -3.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.3, 0.8, curve: Curves.elasticOut),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleTap() {
_controller.forward().then((_) {
_controller.reverse().then((_) {
widget.onPressed();
});
});
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _bounceAnimation.value),
child: Transform.scale(
scale: _scaleAnimation.value,
child: SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: widget.isLoading ? null : _handleTap,
style: ElevatedButton.styleFrom(
backgroundColor: widget.backgroundColor,
foregroundColor: widget.textColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 5,
),
child: widget.isLoading
? const CircularProgressIndicator(color: Colors.white)
: Text(
widget.text,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
},
);
}
}
class EntrarSheet extends StatefulWidget {
const EntrarSheet({super.key});
@override
State<EntrarSheet> createState() => _EntrarSheetState();
}
class _EntrarSheetState extends State<EntrarSheet> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
Future<void> _handleLogin() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
try {
await SupabaseService.signIn(
email: _emailController.text.trim(),
password: _passwordController.text,
);
if (mounted) {
// Close the bottom sheet first
Navigator.of(context).pop();
// Show success message above the sheet
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login realizado com sucesso!'),
backgroundColor: Colors.green,
),
);
// Then navigate to LogadoInicialScreen
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const LogadoInicialScreen(),
),
);
}
} catch (e) {
if (mounted) {
// Close the bottom sheet first
Navigator.of(context).pop();
// Show error message above the sheet
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString()), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
}
Future<void> _handlePasswordReset() async {
final email = _emailController.text.trim();
if (email.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Por favor, insira seu email'),
backgroundColor: Colors.orange,
),
);
return;
}
try {
await SupabaseService.resetPassword(email);
if (mounted) {
// Close the bottom sheet first
Navigator.of(context).pop();
// Show success message above the sheet
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Email de redefinição enviado!'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
// Close the bottom sheet first
Navigator.of(context).pop();
// Show error message above the sheet
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString()), backgroundColor: Colors.red),
);
}
}
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.6, // Start at 60% for login form
minChildSize: 0.4, // 40% minimum
maxChildSize: 0.9, // 90% maximum
snap: true, // Enable snap points
snapSizes: [0.6, 0.9], // Snap to 60% or 90%
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: AppColors.backgroundGrey,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -5),
),
],
),
child: SingleChildScrollView(
controller: scrollController,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Drag handle
Center(
child: Container(
width: 60,
height: 6,
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(height: 24),
// Title
const Text(
'Entrar',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 32),
// Email field
const Text(
'Email',
style: TextStyle(fontSize: 16, color: Colors.white70),
),
const SizedBox(height: 8),
Form(
key: _formKey,
child: TextFormField(
controller: _emailController,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
hintText: 'seu@email.com',
hintStyle: const TextStyle(color: Colors.white38),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor, insira seu email';
}
if (!value.contains('@')) {
return 'Email inválido';
}
return null;
},
),
),
const SizedBox(height: 20),
// Password field
const Text(
'Senha',
style: TextStyle(fontSize: 16, color: Colors.white70),
),
const SizedBox(height: 8),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
hintText: '••••••••',
hintStyle: const TextStyle(color: Colors.white38),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor, insira sua senha';
}
if (value.length < 6) {
return 'Senha deve ter pelo menos 6 caracteres';
}
return null;
},
),
const SizedBox(height: 32),
// Login button
AnimatedButton(
text: 'Entrar',
onPressed: _handleLogin,
backgroundColor: AppColors.buttonColor,
textColor: AppColors.white,
isLoading: _isLoading,
),
const SizedBox(height: 20),
// Forgot password
Center(
child: TextButton(
onPressed: _handlePasswordReset,
child: const Text(
'Esqueceu a senha?',
style: TextStyle(color: Colors.white70, fontSize: 16),
),
),
),
const SizedBox(height: 40),
],
),
),
),
),
);
},
);
}
}

View File

@@ -0,0 +1,386 @@
import 'package:flutter/material.dart';
import '../constants/app_colors.dart';
import '../services/supabase_service.dart';
import '../screens/logado_inicial_screen.dart';
class AnimatedButton extends StatefulWidget {
final String text;
final VoidCallback onPressed;
final Color backgroundColor;
final Color textColor;
final bool isLoading;
const AnimatedButton({
super.key,
required this.text,
required this.onPressed,
required this.backgroundColor,
required this.textColor,
this.isLoading = false,
});
@override
State<AnimatedButton> createState() => _AnimatedButtonState();
}
class _AnimatedButtonState extends State<AnimatedButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
late Animation<double> _bounceAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 150),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: 0.92,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
_bounceAnimation = Tween<double>(begin: 0.0, end: -3.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.3, 0.8, curve: Curves.elasticOut),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleTap() {
_controller.forward().then((_) {
_controller.reverse().then((_) {
widget.onPressed();
});
});
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _bounceAnimation.value),
child: Transform.scale(
scale: _scaleAnimation.value,
child: SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: widget.isLoading ? null : _handleTap,
style: ElevatedButton.styleFrom(
backgroundColor: widget.backgroundColor,
foregroundColor: widget.textColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 5,
),
child: widget.isLoading
? const CircularProgressIndicator(color: Colors.white)
: Text(
widget.text,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
),
),
),
);
},
);
}
}
class RegistrarSheet extends StatefulWidget {
const RegistrarSheet({super.key});
@override
State<RegistrarSheet> createState() => _RegistrarSheetState();
}
class _RegistrarSheetState extends State<RegistrarSheet> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _isLoading = false;
Future<void> _handleRegister() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
print(
'DEBUG: Dados do formulário - Nome: ${_nameController.text}, Email: ${_emailController.text}',
);
try {
await SupabaseService.signUp(
email: _emailController.text.trim(),
password: _passwordController.text,
name: _nameController.text.trim(),
);
if (mounted) {
// Close the bottom sheet first
Navigator.of(context).pop();
// Show success message above the sheet
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Conta criada com sucesso! Verifique seu email.'),
backgroundColor: Colors.green,
),
);
// Then navigate to LogadoInicialScreen
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const LogadoInicialScreen(),
),
);
}
} catch (e) {
print('DEBUG: Erro no registro: $e');
if (mounted) {
// Close the bottom sheet first
Navigator.of(context).pop();
// Show error message above the sheet
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString()), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.7, // Start at 70% for register form
minChildSize: 0.5, // 50% minimum
maxChildSize: 0.95, // 95% maximum
snap: true, // Enable snap points
snapSizes: [0.7, 0.95], // Snap to 70% or 95%
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
color: AppColors.backgroundGrey,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -5),
),
],
),
child: SingleChildScrollView(
controller: scrollController,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: _buildRegistrationForm(),
),
),
),
);
},
);
}
Widget _buildRegistrationForm() {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Drag handle
Center(
child: Container(
width: 60,
height: 6,
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(height: 24),
// Title
const Text(
'Registrar',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 32),
// Name field
const Text(
'Nome',
style: TextStyle(fontSize: 16, color: Colors.white70),
),
const SizedBox(height: 8),
TextFormField(
controller: _nameController,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
hintText: 'Seu nome completo',
hintStyle: const TextStyle(color: Colors.white38),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor, insira seu nome';
}
if (value.length < 3) {
return 'Nome deve ter pelo menos 3 caracteres';
}
return null;
},
),
const SizedBox(height: 20),
// Email field
const Text(
'Email',
style: TextStyle(fontSize: 16, color: Colors.white70),
),
const SizedBox(height: 8),
TextFormField(
controller: _emailController,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
hintText: 'seu@email.com',
hintStyle: const TextStyle(color: Colors.white38),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor, insira seu email';
}
if (!value.contains('@')) {
return 'Email inválido';
}
return null;
},
),
const SizedBox(height: 20),
// Password field
const Text(
'Senha',
style: TextStyle(fontSize: 16, color: Colors.white70),
),
const SizedBox(height: 8),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
hintText: '••••••••',
hintStyle: const TextStyle(color: Colors.white38),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor, insira sua senha';
}
if (value.length < 6) {
return 'Senha deve ter pelo menos 6 caracteres';
}
return null;
},
),
const SizedBox(height: 20),
// Confirm password field
const Text(
'Confirmar Senha',
style: TextStyle(fontSize: 16, color: Colors.white70),
),
const SizedBox(height: 8),
TextFormField(
controller: _confirmPasswordController,
obscureText: true,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
hintText: '••••••••',
hintStyle: const TextStyle(color: Colors.white38),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Por favor, confirme sua senha';
}
if (value != _passwordController.text) {
return 'Senhas não coincidem';
}
return null;
},
),
const SizedBox(height: 32),
// Register button
AnimatedButton(
text: 'Registrar',
onPressed: _handleRegister,
backgroundColor: AppColors.buttonColor,
textColor: AppColors.white,
isLoading: _isLoading,
),
SizedBox(height: 40),
],
),
);
}
}