diff --git a/lib/controllers/game_controller.dart b/lib/controllers/game_controller.dart index 0499a76..91c68b7 100644 --- a/lib/controllers/game_controller.dart +++ b/lib/controllers/game_controller.dart @@ -1,25 +1,40 @@ -import 'dart:async'; +import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/game_model.dart'; class GameController { - final List _games = []; - final _gameStreamController = StreamController>.broadcast(); + final _supabase = Supabase.instance.client; - Stream> get gamesStream => _gameStreamController.stream; + // 1. LER JOGOS (Stream em Tempo Real) + Stream> get gamesStream { + return _supabase + .from('games') + .stream(primaryKey: ['id']) + .order('game_date', ascending: false) // Mais recentes primeiro + .map((data) => data.map((json) => Game.fromMap(json)).toList()); + } - void addGame(String myTeam, String opponent, String season) { - final newGame = Game( - id: DateTime.now().toString(), - myTeam: myTeam, - opponentTeam: opponent, - season: season, - date: DateTime.now(), - ); - _games.insert(0, newGame); // Adiciona ao topo da lista - _gameStreamController.add(List.unmodifiable(_games)); + // 2. CRIAR JOGO + // Retorna o ID do jogo criado para podermos navegar para o placar + Future createGame(String myTeam, String opponent, String season) async { + try { + final response = await _supabase.from('games').insert({ + 'my_team': myTeam, + 'opponent_team': opponent, + 'season': season, + 'my_score': 0, + 'opponent_score': 0, + 'status': 'Decorrer', // Começa como "Decorrer" + 'game_date': DateTime.now().toIso8601String(), + }).select().single(); // .select().single() retorna o objeto criado + + return response['id']; // Retorna o UUID gerado pelo Supabase + } catch (e) { + print("Erro ao criar jogo: $e"); + return null; + } } void dispose() { - _gameStreamController.close(); + // Não é necessário fechar streams do Supabase manualmente aqui } } \ No newline at end of file diff --git a/lib/models/game_model.dart b/lib/models/game_model.dart index 6d95d8c..1d836b2 100644 --- a/lib/models/game_model.dart +++ b/lib/models/game_model.dart @@ -12,10 +12,27 @@ class Game { required this.id, required this.myTeam, required this.opponentTeam, - this.myScore = "0", - this.opponentScore = "0", + required this.myScore, + required this.opponentScore, required this.season, - this.status = "Brevemente", + required this.status, required this.date, }); + + // Converte dados do Supabase para o Objeto Dart + factory Game.fromMap(Map map) { + return Game( + id: map['id'] ?? '', + myTeam: map['my_team'] ?? 'Desconhecido', + opponentTeam: map['opponent_team'] ?? 'Desconhecido', + // Convertemos para String porque no DB é Integer, mas na UI usas String + myScore: (map['my_score'] ?? 0).toString(), + opponentScore: (map['opponent_score'] ?? 0).toString(), + season: map['season'] ?? '', + status: map['status'] ?? 'Brevemente', + date: map['game_date'] != null + ? DateTime.parse(map['game_date']) + : DateTime.now(), + ); + } } \ No newline at end of file diff --git a/lib/pages/PlacarPage.dart b/lib/pages/PlacarPage.dart index 5fead31..0c29afd 100644 --- a/lib/pages/PlacarPage.dart +++ b/lib/pages/PlacarPage.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class PlacarPage extends StatefulWidget { final String gameId; @@ -21,11 +22,20 @@ class _PlacarPageState extends State { int _myScore = 0; int _opponentScore = 0; - // Lógica do Tempo (Exemplo: 10 minutos) Duration _duration = const Duration(minutes: 10); Timer? _timer; bool _isRunning = false; + @override + void initState() { + super.initState(); + // Força a tela na horizontal + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeRight, + DeviceOrientation.landscapeLeft, + ]); + } + void _toggleTimer() { if (_isRunning) { _timer?.cancel(); @@ -49,91 +59,205 @@ class _PlacarPageState extends State { @override void dispose() { _timer?.cancel(); + // Restaura a vertical ao sair + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFF121212), - appBar: AppBar( - title: const Text("Placar Live"), - backgroundColor: Colors.transparent, - foregroundColor: Colors.white, - elevation: 0, - ), - body: Column( + body: Stack( children: [ - const SizedBox(height: 20), - // Cronómetro - GestureDetector( - onTap: _toggleTimer, - child: Text( - _formatTime(_duration), - style: const TextStyle(color: Colors.white, fontSize: 75, fontWeight: FontWeight.bold, fontFamily: 'monospace'), - ), - ), - const Text("CLIQUE NO TEMPO PARA INICIAR/PAUSAR", style: TextStyle(color: Colors.grey, fontSize: 10)), - - Expanded( - child: Row( - children: [ - // Minha Equipa - _buildTeamSide(widget.myTeam, _myScore, (p) => setState(() => _myScore += p), const Color(0xFFE74C3C)), - // Divisor - Container(width: 1, color: Colors.white24, margin: const EdgeInsets.symmetric(vertical: 40)), - // Adversário - _buildTeamSide(widget.opponentTeam, _opponentScore, (p) => setState(() => _opponentScore += p), Colors.blueGrey), - ], + // 1. FUNDO (O CAMPO DE BASQUETEBOL) + Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + color: Color(0xFFD2B48C), // Cor de madeira temporária + // TODO: Descomenta a linha abaixo e usa a tua imagem do campo + // image: DecorationImage(image: AssetImage('assets/court.png'), fit: BoxFit.cover), ), + // Linhas do campo desenhadas por cima (Opcional se usares imagem real) + child: const CustomPaint(painter: CourtPainterPlaceholder()), ), - // Botão Finalizar - Padding( - padding: const EdgeInsets.all(20.0), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - minimumSize: const Size(double.infinity, 50), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), - ), - onPressed: () { - // Aqui podes adicionar a lógica para salvar o resultado final no Controller - Navigator.pop(context); - }, - child: const Text("FINALIZAR PARTIDA", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), - ), + // 2. BARRA SUPERIOR (PLACARD) + Positioned( + top: 0, + left: 0, + right: 0, + child: _buildTopScoreboard(), ), - ], - ), - ); - } - Widget _buildTeamSide(String name, int score, Function(int) onAdd, Color color) { - return Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(name, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(height: 10), - Text("$score", style: TextStyle(color: color, fontSize: 80, fontWeight: FontWeight.bold)), - const SizedBox(height: 30), - // Botões de Pontuação - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [1, 2, 3].map((p) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: InkWell( - onTap: () => onAdd(p), - child: CircleAvatar( - backgroundColor: color.withOpacity(0.2), - child: Text("+$p", style: TextStyle(color: color, fontWeight: FontWeight.bold)), + // 3. JOGADORES NO CAMPO (Posições manuais usando Positioned) + // Exemplo de como posicionar os teus jogadores + Positioned(top: 120, left: 100, child: _buildPlayerCard("6", "LeBron James")), + Positioned(top: 250, left: 160, child: _buildPlayerCard("3", "Anthony Davis")), + Positioned(top: 320, left: 280, child: _buildPlayerCard("28", "Rui Hachimura")), + Positioned(top: 380, left: 160, child: _buildPlayerCard("1", "D'Angelo Russell")), + Positioned(top: 500, left: 100, child: _buildPlayerCard("15", "Austin Reaves")), + + // 4. BOTÃO CENTRAL (PLAY/PAUSE) + Center( + child: GestureDetector( + onTap: _toggleTimer, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFF2662D9).withOpacity(0.8), + border: Border.all(color: Colors.blueAccent.withOpacity(0.5), width: 8), + ), + padding: const EdgeInsets.all(16), + child: Icon( + _isRunning ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 40, ), ), - )).toList(), + ), + ), + + // 5. PAINEL DE ACÇÕES EM BAIXO (Estatísticas: 1pt, 2pt, 3pt) + Positioned( + bottom: 20, + left: 0, + right: 0, + child: _buildActionButtonsPanel(), + ), + + // 6. BOTÃO DE FECHAR / GUARDAR NO CANTO + Positioned( + bottom: 20, + right: 20, + child: FloatingActionButton( + backgroundColor: const Color(0xFF1E2A38), + mini: true, + onPressed: () => Navigator.pop(context), + child: const Icon(Icons.close, color: Colors.white), + ), ), ], ), ); } + + // --- WIDGETS AUXILIARES PARA LIMPAR O CÓDIGO --- + + Widget _buildTopScoreboard() { + return Container( + color: const Color(0xFF1E2A38), // Azul Escuro da barra + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + child: SafeArea( + bottom: false, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Equipa Casa + Text(widget.myTeam.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + const SizedBox(width: 15), + _scoreBox(_myScore, const Color(0xFF2662D9)), // Azul + + const SizedBox(width: 20), + + // Relógio + Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + decoration: BoxDecoration(color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(8)), + child: Text( + _formatTime(_duration), + style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, fontFamily: 'monospace'), + ), + ), + + const SizedBox(width: 20), + + // Equipa Visitante + _scoreBox(_opponentScore, const Color(0xFFE74C3C)), // Vermelho + const SizedBox(width: 15), + Text(widget.opponentTeam.toUpperCase(), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ], + ), + ), + ); + } + + Widget _scoreBox(int score, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(8)), + child: Text(score.toString(), style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)), + ); + } + + Widget _buildPlayerCard(String number, String name) { + return Container( + decoration: BoxDecoration( + color: const Color(0xFF1E2A38).withOpacity(0.9), + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + backgroundColor: const Color(0xFF2662D9), + radius: 12, + child: Text(number, style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), + ), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold)), + const Text("0 Pts | 0 Rbs | 0 Ast", style: TextStyle(color: Colors.grey, fontSize: 8)), + ], + ) + ], + ), + ); + } + + Widget _buildActionButtonsPanel() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Podes adicionar onTap nestes botões para chamarem setState(() => _myScore += X) + _actionBtn("1", Colors.orange), + _actionBtn("2", Colors.orange), + _actionBtn("3", Colors.orange), + const SizedBox(width: 20), + _actionBtn("M", Colors.blueGrey, icon: Icons.sports_basketball), + _actionBtn("F", Colors.redAccent, icon: Icons.pan_tool), + ], + ); + } + + Widget _actionBtn(String label, Color color, {IconData? icon}) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + padding: const EdgeInsets.all(12), + child: icon != null + ? Icon(icon, color: Colors.white, size: 20) + : Text(label, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)), + ); + } +} + +// Classe temporária para desenhar as marcações do campo caso não tenhas imagem +class CourtPainterPlaceholder extends CustomPainter { + const CourtPainterPlaceholder(); + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = Colors.white.withOpacity(0.5)..style = PaintingStyle.stroke..strokeWidth = 2; + canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint); + canvas.drawLine(Offset(size.width / 2, 0), Offset(size.width / 2, size.height), paint); + canvas.drawCircle(Offset(size.width / 2, size.height / 2), 60, paint); + } + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } \ No newline at end of file diff --git a/lib/pages/gamePage.dart b/lib/pages/gamePage.dart index 0632e1f..613ae86 100644 --- a/lib/pages/gamePage.dart +++ b/lib/pages/gamePage.dart @@ -12,17 +12,9 @@ class GamePage extends StatefulWidget { } class _GamePageState extends State { - // Criamos as instâncias dos controllers final GameController gameController = GameController(); final TeamController teamController = TeamController(); - @override - void dispose() { - // É importante fechar os streams quando a página sai da memória - gameController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -33,12 +25,17 @@ class _GamePageState extends State { elevation: 0, ), body: StreamBuilder>( + // LÊ DIRETAMENTE DO SUPABASE stream: gameController.gamesStream, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } + if (snapshot.hasError) { + return Center(child: Text("Erro: ${snapshot.error}")); + } + if (!snapshot.hasData || snapshot.data!.isEmpty) { return const Center(child: Text("Nenhum jogo registado.")); } @@ -49,7 +46,6 @@ class _GamePageState extends State { itemBuilder: (context, index) { final game = snapshot.data![index]; - // ATUALIZADO: Passamos o gameId para o card return GameResultCard( gameId: game.id, myTeam: game.myTeam, @@ -75,10 +71,8 @@ class _GamePageState extends State { showDialog( context: context, builder: (context) => CreateGameDialogManual( - controller: teamController, - onConfirm: (my, opp, sea) { - gameController.addGame(my, opp, sea); - }, + teamController: teamController, + gameController: gameController, // Passamos o controller para fazer o insert ), ); } diff --git a/lib/widgets/game_widgets.dart b/lib/widgets/game_widgets.dart index 71e4089..09a0bd1 100644 --- a/lib/widgets/game_widgets.dart +++ b/lib/widgets/game_widgets.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:playmaker/pages/PlacarPage.dart'; +import 'package:playmaker/pages/PlacarPage.dart'; // Garante que o import está correto import '../controllers/team_controller.dart'; +import '../controllers/game_controller.dart'; // Import necessário -// --- CARD DE EXIBIÇÃO DO JOGO --- +// --- CARD DE EXIBIÇÃO DO JOGO (Mantém-se quase igual) --- class GameResultCard extends StatelessWidget { - final String gameId; // Adicionado para identificar o jogo no retorno + final String gameId; final String myTeam, opponentTeam, myScore, opponentScore, status, season; const GameResultCard({ @@ -26,15 +27,14 @@ class GameResultCard extends StatelessWidget { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), - boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10)], + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 10)], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _buildTeamInfo(myTeam, const Color(0xFFE74C3C)), - // Agora passamos o context e o gameId para o centro - _buildScoreCenter(context, gameId), - _buildTeamInfo(opponentTeam, Colors.black87), + Expanded(child: _buildTeamInfo(myTeam, const Color(0xFFE74C3C))), + _buildScoreCenter(context, gameId), + Expanded(child: _buildTeamInfo(opponentTeam, Colors.black87)), ], ), ); @@ -43,12 +43,13 @@ class GameResultCard extends StatelessWidget { Widget _buildTeamInfo(String name, Color color) { return Column( children: [ - CircleAvatar( - backgroundColor: color, - child: const Icon(Icons.shield, color: Colors.white) - ), + CircleAvatar(backgroundColor: color, child: const Icon(Icons.shield, color: Colors.white)), const SizedBox(height: 4), - Text(name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), + Text(name, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), ], ); } @@ -65,22 +66,22 @@ class GameResultCard extends StatelessWidget { ], ), const SizedBox(height: 8), - - // BOTÃO PARA RETORNAR AO JOGO TextButton.icon( onPressed: () { - print("Navegando para o marcador do jogo: $id"); - // Aqui faremos a navegação para a página do marcador em breve + // NAVEGAÇÃO PARA O PLACAR (Usando o ID real) + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PlacarPage( + gameId: id, + myTeam: myTeam, + opponentTeam: opponentTeam, + ), + ), + ); }, icon: const Icon(Icons.play_circle_fill, size: 16, color: Color(0xFFE74C3C)), - label: const Text( - "RETORNAR", - style: TextStyle( - fontSize: 10, - color: Color(0xFFE74C3C), - fontWeight: FontWeight.bold - ), - ), + label: const Text("RETORNAR", style: TextStyle(fontSize: 10, color: Color(0xFFE74C3C), fontWeight: FontWeight.bold)), style: TextButton.styleFrom( backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1), padding: const EdgeInsets.symmetric(horizontal: 12), @@ -88,7 +89,6 @@ class GameResultCard extends StatelessWidget { visualDensity: VisualDensity.compact, ), ), - const SizedBox(height: 4), Text(status, style: const TextStyle(fontSize: 10, color: Colors.blue, fontWeight: FontWeight.bold)), ], @@ -102,12 +102,16 @@ class GameResultCard extends StatelessWidget { ); } -// --- POPUP DE CRIAÇÃO --- +// --- POPUP DE CRIAÇÃO (MODIFICADO PARA SUPABASE) --- class CreateGameDialogManual extends StatefulWidget { - final TeamController controller; - final Function(String, String, String) onConfirm; + final TeamController teamController; + final GameController gameController; // Recebemos o controller do jogo - const CreateGameDialogManual({super.key, required this.controller, required this.onConfirm}); + const CreateGameDialogManual({ + super.key, + required this.teamController, + required this.gameController + }); @override State createState() => _CreateGameDialogManualState(); @@ -115,8 +119,10 @@ class CreateGameDialogManual extends StatefulWidget { class _CreateGameDialogManualState extends State { late TextEditingController _seasonController; - String _myTeamName = ""; - String _opponentName = ""; + final TextEditingController _myTeamController = TextEditingController(); + final TextEditingController _opponentController = TextEditingController(); + + bool _isLoading = false; // Para mostrar loading no botão @override void initState() { @@ -124,12 +130,6 @@ class _CreateGameDialogManualState extends State { _seasonController = TextEditingController(text: _calculateSeason()); } - @override - void dispose() { - _seasonController.dispose(); - super.dispose(); - } - String _calculateSeason() { final now = DateTime.now(); return now.month >= 7 ? "${now.year}/${(now.year + 1).toString().substring(2)}" : "${now.year - 1}/${now.year.toString().substring(2)}"; @@ -146,73 +146,102 @@ class _CreateGameDialogManualState extends State { children: [ TextField( controller: _seasonController, - decoration: const InputDecoration( - labelText: 'Temporada', - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.calendar_today, size: 20), - ), + decoration: const InputDecoration(labelText: 'Temporada', border: OutlineInputBorder(), prefixIcon: Icon(Icons.calendar_today)), ), const SizedBox(height: 15), - _buildSearch(label: "Minha Equipa", onSelect: (v) => _myTeamName = v), + + // Usamos Autocomplete para equipas (Assumindo que TeamController já é Supabase) + _buildSearch(label: "Minha Equipa", controller: _myTeamController), + const Padding(padding: EdgeInsets.symmetric(vertical: 8), child: Text("VS", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey))), - _buildSearch(label: "Adversário", onSelect: (v) => _opponentName = v), + + _buildSearch(label: "Adversário", controller: _opponentController), ], ), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('CANCELAR')), + ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFE74C3C), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - onPressed: () { - if (_myTeamName.isNotEmpty && _opponentName.isNotEmpty) { - // 1. Criar um ID único para este jogo - final String newGameId = DateTime.now().toString(); - - // 2. Notificar a GamePage para criar o card (via onConfirm) - widget.onConfirm(_myTeamName, _opponentName, _seasonController.text); - - // 3. Fechar o popup de criação - Navigator.pop(context); - - // 4. Ir direto para a tela do marcador de pontos - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PlacarPage( - gameId: newGameId, - myTeam: _myTeamName, - opponentTeam: _opponentName, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE74C3C), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), + onPressed: _isLoading ? null : () async { + if (_myTeamController.text.isNotEmpty && _opponentController.text.isNotEmpty) { + setState(() => _isLoading = true); + + // 1. CRIAR NO SUPABASE E OBTER O ID REAL + String? newGameId = await widget.gameController.createGame( + _myTeamController.text, + _opponentController.text, + _seasonController.text, + ); + + setState(() => _isLoading = false); + + if (newGameId != null && context.mounted) { + // 2. Fechar Popup + Navigator.pop(context); + + // 3. Ir para o Placar com o ID real do banco + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PlacarPage( + gameId: newGameId, + myTeam: _myTeamController.text, + opponentTeam: _opponentController.text, + ), + ), + ); + } + } + }, + child: _isLoading + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) + : const Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), ), - ); - } - }, - child: const Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), -), ], ); } - Widget _buildSearch({required String label, required Function(String) onSelect}) { + Widget _buildSearch({required String label, required TextEditingController controller}) { return StreamBuilder>>( - stream: widget.controller.teamsStream, + stream: widget.teamController.teamsStream, builder: (context, snapshot) { - List teamList = snapshot.hasData ? snapshot.data!.map((t) => t['name'].toString()).toList() : []; + List teamList = snapshot.hasData + ? snapshot.data!.map((t) => t['name'].toString()).toList() + : []; + return Autocomplete( - optionsBuilder: (val) => val.text.isEmpty ? [] : teamList.where((t) => t.toLowerCase().contains(val.text.toLowerCase())), - fieldViewBuilder: (ctx, ctrl, node, submit) => TextField( - controller: ctrl, - focusNode: node, - onChanged: onSelect, - decoration: InputDecoration( - labelText: label, - prefixIcon: const Icon(Icons.search), - border: const OutlineInputBorder() - ), - ), + optionsBuilder: (val) { + if (val.text.isEmpty) return const Iterable.empty(); + return teamList.where((t) => t.toLowerCase().contains(val.text.toLowerCase())); + }, + onSelected: (String selection) { + controller.text = selection; + }, + fieldViewBuilder: (ctx, txtCtrl, node, submit) { + // Sincronizar o controller interno do Autocomplete com o nosso controller externo + if (txtCtrl.text.isEmpty && controller.text.isNotEmpty) { + txtCtrl.text = controller.text; + } + // Importante: Guardar o valor escrito mesmo que não selecionado da lista + txtCtrl.addListener(() { + controller.text = txtCtrl.text; + }); + + return TextField( + controller: txtCtrl, + focusNode: node, + decoration: InputDecoration( + labelText: label, + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder() + ), + ); + }, ); }, );