TUDO
This commit is contained in:
14
lib/constants/app_colors.dart
Normal file
14
lib/constants/app_colors.dart
Normal 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);
|
||||
}
|
||||
10
lib/constants/app_constants.dart
Normal file
10
lib/constants/app_constants.dart
Normal 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';
|
||||
}
|
||||
8
lib/constants/app_strings.dart
Normal file
8
lib/constants/app_strings.dart
Normal 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
46
lib/main.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
891
lib/screens/google_maps_screen.dart
Normal file
891
lib/screens/google_maps_screen.dart
Normal 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'
|
||||
'®ion=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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
155
lib/screens/inicial_screen.dart
Normal file
155
lib/screens/inicial_screen.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
652
lib/screens/logado_inicial_screen.dart
Normal file
652
lib/screens/logado_inicial_screen.dart
Normal 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;
|
||||
}
|
||||
219
lib/screens/logado_screen.dart
Normal file
219
lib/screens/logado_screen.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
414
lib/screens/setting_screen.dart
Normal file
414
lib/screens/setting_screen.dart
Normal 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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
130
lib/services/supabase_service.dart
Normal file
130
lib/services/supabase_service.dart
Normal 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;
|
||||
}
|
||||
360
lib/sheets/entrar_sheet.dart
Normal file
360
lib/sheets/entrar_sheet.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
386
lib/sheets/registrar_sheet.dart
Normal file
386
lib/sheets/registrar_sheet.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user