ndk
This commit is contained in:
@@ -8,8 +8,8 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
namespace = "com.example.playmaker"
|
namespace = "com.example.playmaker"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
//ndkVersion = flutter.ndkVersion
|
||||||
|
ndkVersion = "26.1.10909125"
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
@@ -39,12 +39,14 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Adicione no FINAL do arquivo:
|
// Adicione no FINAL do arquivo:
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply(plugin = "com.google.gms.google-services")
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Dependências do Firebase que você vai usar
|
// Usa parênteses e aspas duplas
|
||||||
implementation 'com.google.firebase:firebase-analytics'
|
implementation("com.google.firebase:firebase-analytics")
|
||||||
// Adicione outras conforme necessário
|
|
||||||
|
// Se tiveres o BOM do Firebase, também deve ser assim:
|
||||||
|
implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
// Esta linha permite que o Gradle encontre o plugin do Google Services
|
||||||
|
classpath("com.google.gms:google-services:4.4.0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@@ -15,6 +26,7 @@ subprojects {
|
|||||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
project.evaluationDependsOn(":app")
|
project.evaluationDependsOn(":app")
|
||||||
}
|
}
|
||||||
@@ -22,9 +34,3 @@ subprojects {
|
|||||||
tasks.register<Delete>("clean") {
|
tasks.register<Delete>("clean") {
|
||||||
delete(rootProject.layout.buildDirectory)
|
delete(rootProject.layout.buildDirectory)
|
||||||
}
|
}
|
||||||
buildscript {
|
|
||||||
dependencies {
|
|
||||||
// Adicione esta linha:
|
|
||||||
classpath 'com.google.gms:google-services:4.4.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ plugins {
|
|||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.9.1" apply false
|
id("com.android.application") version "8.9.1" apply false
|
||||||
// START: FlutterFire Configuration
|
// START: FlutterFire Configuration
|
||||||
id("com.google.gms.google-services") version("4.3.15") apply false
|
id("com.google.gms.google-services") version "4.3.15" apply false
|
||||||
// END: FlutterFire Configuration
|
// END: FlutterFire Configuration
|
||||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|||||||
43
ios/Podfile
Normal file
43
ios/Podfile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Uncomment this line to define a global platform for your project
|
||||||
|
# platform :ios, '13.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
25
lib/controllers/game_controller.dart
Normal file
25
lib/controllers/game_controller.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import '../models/game_model.dart';
|
||||||
|
|
||||||
|
class GameController {
|
||||||
|
final List<Game> _games = [];
|
||||||
|
final _gameStreamController = StreamController<List<Game>>.broadcast();
|
||||||
|
|
||||||
|
Stream<List<Game>> get gamesStream => _gameStreamController.stream;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_gameStreamController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,18 +5,18 @@ import '../models/person_model.dart';
|
|||||||
class StatsController {
|
class StatsController {
|
||||||
final SupabaseClient _supabase = Supabase.instance.client;
|
final SupabaseClient _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
// --- 1. LER MEMBROS (STREAM EM TEMPO REAL) ---
|
// 1. LER
|
||||||
Stream<List<Person>> getMembers(String teamId) {
|
Stream<List<Person>> getMembers(String teamId) {
|
||||||
return _supabase
|
return _supabase
|
||||||
.from('members') // Nome da tua tabela no Supabase
|
.from('members')
|
||||||
.stream(primaryKey: ['id'])
|
.stream(primaryKey: ['id'])
|
||||||
.eq('team_id', teamId)
|
.eq('team_id', teamId)
|
||||||
.order('name', ascending: true)
|
.order('name', ascending: true)
|
||||||
.map((data) => data.map((json) => Person.fromMap(json)).toList());
|
.map((data) => data.map((json) => Person.fromMap(json)).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. APAGAR ---
|
// 2. APAGAR
|
||||||
Future<void> deletePerson(String teamId, String personId) async {
|
Future<void> deletePerson(String personId) async {
|
||||||
try {
|
try {
|
||||||
await _supabase.from('members').delete().eq('id', personId);
|
await _supabase.from('members').delete().eq('id', personId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -24,112 +24,135 @@ class StatsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. DIÁLOGOS DE ADICIONAR / EDITAR ---
|
// 3. DIÁLOGOS
|
||||||
|
|
||||||
// Abrir diálogo para criar novo
|
|
||||||
void showAddPersonDialog(BuildContext context, String teamId) {
|
void showAddPersonDialog(BuildContext context, String teamId) {
|
||||||
_showPersonForm(context, teamId: teamId);
|
_showForm(context, teamId: teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abrir diálogo para editar existente
|
|
||||||
void showEditPersonDialog(BuildContext context, String teamId, Person person) {
|
void showEditPersonDialog(BuildContext context, String teamId, Person person) {
|
||||||
_showPersonForm(context, teamId: teamId, person: person);
|
_showForm(context, teamId: teamId, person: person);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lógica interna do formulário
|
// --- O POPUP ESTÁ AQUI ---
|
||||||
void _showPersonForm(BuildContext context, {required String teamId, Person? person}) {
|
void _showForm(BuildContext context, {required String teamId, Person? person}) {
|
||||||
final isEditing = person != null;
|
final isEdit = person != null;
|
||||||
|
final nameCtrl = TextEditingController(text: person?.name ?? '');
|
||||||
|
final numCtrl = TextEditingController(text: person?.number ?? '');
|
||||||
|
|
||||||
// Controladores de texto
|
// Define o valor inicial
|
||||||
final nameController = TextEditingController(text: person?.name ?? '');
|
|
||||||
final numberController = TextEditingController(text: person?.number ?? '');
|
|
||||||
|
|
||||||
// Variável para o Dropdown (Valor inicial)
|
|
||||||
String selectedType = person?.type ?? 'Jogador';
|
String selectedType = person?.type ?? 'Jogador';
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (ctx) => StatefulBuilder(
|
||||||
// StatefulBuilder serve para atualizar o Dropdown DENTRO do diálogo
|
builder: (ctx, setState) => AlertDialog(
|
||||||
return StatefulBuilder(
|
title: Text(isEdit ? "Editar" : "Adicionar"),
|
||||||
builder: (context, setState) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text(isEditing ? 'Editar Membro' : 'Novo Membro'),
|
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Nome
|
// NOME
|
||||||
TextField(
|
TextField(
|
||||||
controller: nameController,
|
controller: nameCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'Nome'),
|
decoration: const InputDecoration(labelText: "Nome"),
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
// Tipo (Jogador vs Treinador)
|
// FUNÇÃO
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: selectedType,
|
value: selectedType,
|
||||||
decoration: const InputDecoration(labelText: 'Função'),
|
decoration: const InputDecoration(labelText: "Função"),
|
||||||
items: const [
|
items: ["Jogador", "Treinador"]
|
||||||
DropdownMenuItem(value: 'Jogador', child: Text('Jogador')),
|
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
|
||||||
DropdownMenuItem(value: 'Treinador', child: Text('Treinador')),
|
.toList(),
|
||||||
],
|
onChanged: (v) {
|
||||||
onChanged: (value) {
|
if (v != null) setState(() => selectedType = v);
|
||||||
if (value != null) {
|
|
||||||
setState(() => selectedType = value);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
|
||||||
|
|
||||||
// Número (Só mostramos se for Jogador)
|
// NÚMERO (Só aparece se for Jogador)
|
||||||
if (selectedType == 'Jogador')
|
if (selectedType == "Jogador") ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
TextField(
|
TextField(
|
||||||
controller: numberController,
|
controller: numCtrl,
|
||||||
decoration: const InputDecoration(labelText: 'Número da Camisola'),
|
decoration: const InputDecoration(labelText: "Número da Camisola"),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.text, // Aceita texto para evitar erros
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(ctx),
|
||||||
child: const Text('Cancelar'),
|
child: const Text("Cancelar")
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)),
|
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (nameController.text.trim().isEmpty) return;
|
print("--- 1. CLICOU EM GUARDAR ---");
|
||||||
|
|
||||||
final name = nameController.text.trim();
|
// Validação Simples
|
||||||
final number = numberController.text.trim();
|
if (nameCtrl.text.trim().isEmpty) {
|
||||||
|
print("ERRO: Nome vazio");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEditing) {
|
// Lógica do Número:
|
||||||
// ATUALIZAR
|
// Se for Treinador -> envia NULL
|
||||||
|
// Se for Jogador e estiver vazio -> envia NULL
|
||||||
|
// Se tiver texto -> envia o Texto
|
||||||
|
String? numeroFinal;
|
||||||
|
if (selectedType == "Treinador") {
|
||||||
|
numeroFinal = null;
|
||||||
|
} else {
|
||||||
|
numeroFinal = numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
print("--- 2. DADOS A ENVIAR ---");
|
||||||
|
print("Nome: ${nameCtrl.text}");
|
||||||
|
print("Tipo: $selectedType");
|
||||||
|
print("Número: $numeroFinal");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEdit) {
|
||||||
await _supabase.from('members').update({
|
await _supabase.from('members').update({
|
||||||
'name': name,
|
'name': nameCtrl.text.trim(),
|
||||||
'type': selectedType,
|
'type': selectedType,
|
||||||
'number': number,
|
'number': numeroFinal,
|
||||||
}).eq('id', person!.id);
|
}).eq('id', person!.id);
|
||||||
} else {
|
} else {
|
||||||
// CRIAR NOVO
|
|
||||||
await _supabase.from('members').insert({
|
await _supabase.from('members').insert({
|
||||||
'team_id': teamId,
|
'team_id': teamId, // Verifica se este teamId é válido!
|
||||||
'name': name,
|
'name': nameCtrl.text.trim(),
|
||||||
'type': selectedType,
|
'type': selectedType,
|
||||||
'number': number,
|
'number': numeroFinal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.mounted) Navigator.pop(context);
|
print("--- 3. SUCESSO! FECHANDO DIÁLOGO ---");
|
||||||
},
|
if (ctx.mounted) Navigator.pop(ctx);
|
||||||
child: Text(isEditing ? 'Guardar' : 'Adicionar', style: const TextStyle(color: Colors.white)),
|
|
||||||
|
} catch (e) {
|
||||||
|
print("--- X. ERRO AO GUARDAR ---");
|
||||||
|
print(e.toString());
|
||||||
|
|
||||||
|
// MOSTRA O ERRO NO TELEMÓVEL
|
||||||
|
if (ctx.mounted) {
|
||||||
|
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("Erro: $e"),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
duration: const Duration(seconds: 4),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("Guardar", style: TextStyle(color: Colors.white)),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,80 +1,71 @@
|
|||||||
import 'dart:async';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
class TeamController {
|
class TeamController {
|
||||||
// --- BASE DE DADOS LOCAL (Listas Estáticas) ---
|
// Instância do cliente Supabase
|
||||||
// Mantemos estático para que os dados persistam entre navegações de ecrãs
|
final _supabase = Supabase.instance.client;
|
||||||
static final List<Map<String, dynamic>> _teams = [];
|
|
||||||
static final List<Map<String, dynamic>> _members = [];
|
|
||||||
|
|
||||||
static List<Map<String, dynamic>> get members => _members;
|
// 1. STREAM (Realtime)
|
||||||
|
// Adicionei o .map() no final para garantir que o Dart entende que é uma List<Map>
|
||||||
// StreamController broadcast para permitir múltiplos ouvintes (ex: Home e TeamsPage)
|
|
||||||
final _streamController = StreamController<List<Map<String, dynamic>>>.broadcast();
|
|
||||||
|
|
||||||
// 1. STREAM
|
|
||||||
// Retorna a lista atual mal alguém subscreve
|
|
||||||
Stream<List<Map<String, dynamic>>> get teamsStream {
|
Stream<List<Map<String, dynamic>>> get teamsStream {
|
||||||
_notifyListeners();
|
return _supabase
|
||||||
return _streamController.stream;
|
.from('teams')
|
||||||
|
.stream(primaryKey: ['id'])
|
||||||
|
.order('name', ascending: true)
|
||||||
|
.map((data) => List<Map<String, dynamic>>.from(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. CRIAR
|
// 2. CRIAR
|
||||||
Future<void> createTeam(String name, String season, String imageUrl) async {
|
// Alterei imageUrl para String? (pode ser nulo) para evitar erros se não houver imagem
|
||||||
await Future.delayed(const Duration(milliseconds: 100)); // Simula latência
|
Future<void> createTeam(String name, String season, String? imageUrl) async {
|
||||||
final newTeam = {
|
try {
|
||||||
'id': DateTime.now().millisecondsSinceEpoch.toString(),
|
await _supabase.from('teams').insert({
|
||||||
'name': name,
|
'name': name,
|
||||||
'season': season,
|
'season': season,
|
||||||
'image_url': imageUrl,
|
'image_url': imageUrl,
|
||||||
'is_favorite': false, // Inicializa sempre como falso
|
'is_favorite': false,
|
||||||
};
|
});
|
||||||
_teams.add(newTeam);
|
print("✅ Equipa guardada no Supabase!");
|
||||||
_notifyListeners();
|
} catch (e) {
|
||||||
|
print("❌ Erro ao criar: $e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. ELIMINAR
|
// 3. ELIMINAR
|
||||||
Future<void> deleteTeam(String id) async {
|
Future<void> deleteTeam(String id) async {
|
||||||
_teams.removeWhere((team) => team['id'] == id);
|
try {
|
||||||
_members.removeWhere((member) => member['team_id'] == id);
|
await _supabase.from('teams').delete().eq('id', id);
|
||||||
_notifyListeners();
|
} catch (e) {
|
||||||
|
print("❌ Erro ao eliminar: $e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. FAVORITAR
|
// 4. FAVORITAR
|
||||||
Future<void> toggleFavorite(String teamId) async {
|
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
|
||||||
final index = _teams.indexWhere((t) => t['id'] == teamId);
|
try {
|
||||||
if (index != -1) {
|
await _supabase
|
||||||
// Inverte o valor booleano (trata null como false)
|
.from('teams')
|
||||||
final bool currentStatus = _teams[index]['is_favorite'] ?? false;
|
.update({'is_favorite': !currentStatus}) // Inverte o valor
|
||||||
_teams[index]['is_favorite'] = !currentStatus;
|
.eq('id', teamId);
|
||||||
_notifyListeners();
|
} catch (e) {
|
||||||
|
print("❌ Erro ao favoritar: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. CONTAR JOGADORES
|
// 5. CONTAR JOGADORES
|
||||||
|
// CORRIGIDO: A sintaxe antiga dava erro. O método .count() é o correto agora.
|
||||||
Future<int> getPlayerCount(String teamId) async {
|
Future<int> getPlayerCount(String teamId) async {
|
||||||
return _members.where((m) => m['team_id'] == teamId).length;
|
try {
|
||||||
|
final count = await _supabase
|
||||||
|
.from('members')
|
||||||
|
.count() // Retorna diretamente o número inteiro
|
||||||
|
.eq('team_id', teamId);
|
||||||
|
return count;
|
||||||
|
} catch (e) {
|
||||||
|
print("Erro ao contar jogadores: $e");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. NOTIFICAR E ORDENAR (Única versão corrigida)
|
// Mantemos o dispose vazio para não quebrar a chamada na TeamsPage
|
||||||
void _notifyListeners() {
|
void dispose() {}
|
||||||
if (_streamController.isClosed) return;
|
|
||||||
|
|
||||||
// Ordenação: 1º Favoritos, 2º Nome (Alfabético)
|
|
||||||
_teams.sort((a, b) {
|
|
||||||
final bool favA = a['is_favorite'] ?? false;
|
|
||||||
final bool favB = b['is_favorite'] ?? false;
|
|
||||||
|
|
||||||
if (favA == favB) {
|
|
||||||
return (a['name'] as String).compareTo(b['name'] as String);
|
|
||||||
}
|
|
||||||
return favB ? 1 : -1; // b (favorito) vem antes de a
|
|
||||||
});
|
|
||||||
|
|
||||||
// Enviamos uma CÓPIA da lista (List.from) para garantir que o StreamBuilder detecte a mudança
|
|
||||||
_streamController.add(List.from(_teams));
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
_streamController.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
// TODO Implement this library.
|
|
||||||
21
lib/models/game_model.dart
Normal file
21
lib/models/game_model.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
class Game {
|
||||||
|
final String id;
|
||||||
|
final String myTeam;
|
||||||
|
final String opponentTeam;
|
||||||
|
final String myScore;
|
||||||
|
final String opponentScore;
|
||||||
|
final String season;
|
||||||
|
final String status;
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
Game({
|
||||||
|
required this.id,
|
||||||
|
required this.myTeam,
|
||||||
|
required this.opponentTeam,
|
||||||
|
this.myScore = "0",
|
||||||
|
this.opponentScore = "0",
|
||||||
|
required this.season,
|
||||||
|
this.status = "Brevemente",
|
||||||
|
required this.date,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -13,10 +13,10 @@ class Team {
|
|||||||
this.isFavorite = false
|
this.isFavorite = false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Converte de Mapa (o formato da nossa "memória") para Objeto
|
// Mapeia o JSON que vem do Supabase (id costuma ser UUID ou String)
|
||||||
factory Team.fromMap(Map<String, dynamic> map) {
|
factory Team.fromMap(Map<String, dynamic> map) {
|
||||||
return Team(
|
return Team(
|
||||||
id: map['id'] ?? '',
|
id: map['id']?.toString() ?? '',
|
||||||
name: map['name'] ?? '',
|
name: map['name'] ?? '',
|
||||||
season: map['season'] ?? '',
|
season: map['season'] ?? '',
|
||||||
imageUrl: map['image_url'] ?? '',
|
imageUrl: map['image_url'] ?? '',
|
||||||
@@ -24,10 +24,8 @@ class Team {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converte de Objeto para Mapa (para guardar na lista)
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'id': id,
|
|
||||||
'name': name,
|
'name': name,
|
||||||
'season': season,
|
'season': season,
|
||||||
'image_url': imageUrl,
|
'image_url': imageUrl,
|
||||||
|
|||||||
139
lib/pages/PlacarPage.dart
Normal file
139
lib/pages/PlacarPage.dart
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class PlacarPage extends StatefulWidget {
|
||||||
|
final String gameId;
|
||||||
|
final String myTeam;
|
||||||
|
final String opponentTeam;
|
||||||
|
|
||||||
|
const PlacarPage({
|
||||||
|
super.key,
|
||||||
|
required this.gameId,
|
||||||
|
required this.myTeam,
|
||||||
|
required this.opponentTeam,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PlacarPage> createState() => _PlacarPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlacarPageState extends State<PlacarPage> {
|
||||||
|
int _myScore = 0;
|
||||||
|
int _opponentScore = 0;
|
||||||
|
|
||||||
|
// Lógica do Tempo (Exemplo: 10 minutos)
|
||||||
|
Duration _duration = const Duration(minutes: 10);
|
||||||
|
Timer? _timer;
|
||||||
|
bool _isRunning = false;
|
||||||
|
|
||||||
|
void _toggleTimer() {
|
||||||
|
if (_isRunning) {
|
||||||
|
_timer?.cancel();
|
||||||
|
} else {
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
if (_duration.inSeconds > 0) {
|
||||||
|
setState(() => _duration -= const Duration(seconds: 1));
|
||||||
|
} else {
|
||||||
|
_timer?.cancel();
|
||||||
|
setState(() => _isRunning = false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setState(() => _isRunning = !_isRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTime(Duration d) {
|
||||||
|
return "${d.inMinutes.toString().padLeft(2, '0')}:${d.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
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(
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../controllers/game_controller.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
|
import '../models/game_model.dart';
|
||||||
|
import '../widgets/game_widgets.dart';
|
||||||
|
|
||||||
class GamePage extends StatefulWidget {
|
class GamePage extends StatefulWidget {
|
||||||
const GamePage({super.key});
|
const GamePage({super.key});
|
||||||
@@ -10,103 +12,17 @@ class GamePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _GamePageState extends State<GamePage> {
|
class _GamePageState extends State<GamePage> {
|
||||||
final TeamController controller = TeamController();
|
// Criamos as instâncias dos controllers
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final GameController gameController = GameController();
|
||||||
|
final TeamController teamController = TeamController();
|
||||||
String _selectedSeasonFilter = 'Todas';
|
|
||||||
String _currentSort = 'Recentes';
|
|
||||||
String _searchQuery = '';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
// É importante fechar os streams quando a página sai da memória
|
||||||
|
gameController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showCreateGameDialog(BuildContext context) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return CreateGameDialogManual(controller: controller);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showFilterDialog(BuildContext context) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return StatefulBuilder(
|
|
||||||
builder: (context, setModalState) {
|
|
||||||
return AlertDialog(
|
|
||||||
backgroundColor: Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
||||||
title: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Text("Filtros",
|
|
||||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 18)
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.close, color: Colors.black, size: 20),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Divider(),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildPopupColumn("TEMPORADA", ['Todas', '2023/24', '2024/25', '2025/26'], _selectedSeasonFilter, (val) {
|
|
||||||
setState(() => _selectedSeasonFilter = val);
|
|
||||||
setModalState(() {});
|
|
||||||
}),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
_buildPopupColumn("ORDENAR", ['Recentes', 'Nome'], _currentSort, (val) {
|
|
||||||
setState(() => _currentSort = val);
|
|
||||||
setModalState(() {});
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: const Text("CONCLUÍDO", style: TextStyle(color: Color(0xFFE74C3C), fontWeight: FontWeight.bold)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPopupColumn(String title, List<String> options, String current, Function(String) onSelect) {
|
|
||||||
return Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(title, style: TextStyle(color: Colors.grey[700], fontSize: 11, fontWeight: FontWeight.bold)),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
...options.map((opt) => InkWell(
|
|
||||||
onTap: () => onSelect(opt),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: Text(opt,
|
|
||||||
style: TextStyle(
|
|
||||||
color: current == opt ? const Color(0xFFE74C3C) : Colors.black,
|
|
||||||
fontWeight: current == opt ? FontWeight.bold : FontWeight.normal)),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -115,194 +31,55 @@ class _GamePageState extends State<GamePage> {
|
|||||||
title: const Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold)),
|
title: const Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.filter_list, color: Color(0xFFE74C3C)),
|
|
||||||
onPressed: () => _showFilterDialog(context),
|
|
||||||
),
|
),
|
||||||
],
|
body: StreamBuilder<List<Game>>(
|
||||||
),
|
stream: gameController.gamesStream,
|
||||||
body: Column(
|
builder: (context, snapshot) {
|
||||||
children: [
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
Padding(
|
return const Center(child: CircularProgressIndicator());
|
||||||
padding: const EdgeInsets.all(16.0),
|
}
|
||||||
child: TextField(
|
|
||||||
controller: _searchController,
|
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||||
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
|
return const Center(child: Text("Nenhum jogo registado."));
|
||||||
decoration: InputDecoration(
|
}
|
||||||
hintText: 'Pesquisar jogo...',
|
|
||||||
prefixIcon: const Icon(Icons.search, color: Color(0xFFE74C3C)),
|
return ListView.builder(
|
||||||
filled: true,
|
padding: const EdgeInsets.all(16),
|
||||||
fillColor: Colors.white,
|
itemCount: snapshot.data!.length,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none),
|
itemBuilder: (context, index) {
|
||||||
),
|
final game = snapshot.data![index];
|
||||||
),
|
|
||||||
),
|
// ATUALIZADO: Passamos o gameId para o card
|
||||||
const Expanded(child: Center(child: Text("Nenhum jogo registado."))),
|
return GameResultCard(
|
||||||
],
|
gameId: game.id,
|
||||||
|
myTeam: game.myTeam,
|
||||||
|
opponentTeam: game.opponentTeam,
|
||||||
|
myScore: game.myScore,
|
||||||
|
opponentScore: game.opponentScore,
|
||||||
|
status: game.status,
|
||||||
|
season: game.season,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () => _showCreateGameDialog(context),
|
|
||||||
backgroundColor: const Color(0xFFE74C3C),
|
backgroundColor: const Color(0xFFE74C3C),
|
||||||
child: const Icon(Icons.add, color: Colors.white),
|
child: const Icon(Icons.add, color: Colors.white),
|
||||||
|
onPressed: () => _showCreateDialog(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCreateDialog(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => CreateGameDialogManual(
|
||||||
|
controller: teamController,
|
||||||
|
onConfirm: (my, opp, sea) {
|
||||||
|
gameController.addGame(my, opp, sea);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateGameDialogManual extends StatefulWidget {
|
|
||||||
final TeamController controller;
|
|
||||||
const CreateGameDialogManual({super.key, required this.controller});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
|
||||||
final TextEditingController _seasonController = TextEditingController();
|
|
||||||
|
|
||||||
// Controllers para capturar o texto dos campos de pesquisa
|
|
||||||
String _myTeamName = "";
|
|
||||||
String _opponentName = "";
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_seasonController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- WIDGET DE PESQUISA (AUTOCOMPLETE) ---
|
|
||||||
Widget _buildSearchField({
|
|
||||||
required String label,
|
|
||||||
required List<String> options,
|
|
||||||
required Function(String) onSelected,
|
|
||||||
}) {
|
|
||||||
return Autocomplete<String>(
|
|
||||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
|
||||||
if (textEditingValue.text.isEmpty) {
|
|
||||||
return const Iterable<String>.empty();
|
|
||||||
}
|
|
||||||
return options.where((String option) {
|
|
||||||
return option.toLowerCase().contains(textEditingValue.text.toLowerCase());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSelected: (String selection) {
|
|
||||||
onSelected(selection);
|
|
||||||
FocusScope.of(context).unfocus();
|
|
||||||
},
|
|
||||||
// --- ESTE BLOCO CONSTRÓI A LISTA DE SUGESTÕES EM BAIXO ---
|
|
||||||
optionsViewBuilder: (context, onSelected, options) {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: Material(
|
|
||||||
elevation: 4.0,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: Container(
|
|
||||||
width: MediaQuery.of(context).size.width * 0.7, // Ajusta à largura do dialog
|
|
||||||
constraints: const BoxConstraints(maxHeight: 200), // Limita a altura da lista
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: options.length,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
final String option = options.elementAt(index);
|
|
||||||
return ListTile(
|
|
||||||
title: Text(option),
|
|
||||||
onTap: () => onSelected(option),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
fieldViewBuilder: (context, fieldTextController, focusNode, onFieldSubmitted) {
|
|
||||||
return TextField(
|
|
||||||
controller: fieldTextController,
|
|
||||||
focusNode: focusNode,
|
|
||||||
onChanged: (value) => onSelected(value),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: label,
|
|
||||||
prefixIcon: const Icon(Icons.search, color: Color(0xFFE74C3C)),
|
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.white,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
|
||||||
stream: widget.controller.teamsStream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
// Lista de nomes das equipas que vêm da TeamsPage
|
|
||||||
List<String> teamList = [];
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
teamList = snapshot.data!.map((t) => t['name'].toString()).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return AlertDialog(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
||||||
title: const Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
content: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// Campo da Temporada
|
|
||||||
TextField(
|
|
||||||
controller: _seasonController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Temporada',
|
|
||||||
prefixIcon: const Icon(Icons.calendar_today, size: 20),
|
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Pesquisa: Minha Equipa
|
|
||||||
_buildSearchField(
|
|
||||||
label: "A Minha Equipa",
|
|
||||||
options: teamList,
|
|
||||||
onSelected: (val) => _myTeamName = val,
|
|
||||||
),
|
|
||||||
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 15),
|
|
||||||
child: Text("VS", style: TextStyle( color: Colors.grey, fontSize: 18)),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Pesquisa: Equipa Adversária
|
|
||||||
_buildSearchField(
|
|
||||||
label: "Equipa Adversária",
|
|
||||||
options: teamList,
|
|
||||||
onSelected: (val) => _opponentName = val,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('CANCELAR')),
|
|
||||||
ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: const Color(0xFFE74C3C),
|
|
||||||
minimumSize: const Size(100, 45),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
if (_myTeamName.isNotEmpty && _opponentName.isNotEmpty) {
|
|
||||||
// Lógica para iniciar o jogo
|
|
||||||
print("Iniciando: $_myTeamName VS $_opponentName");
|
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text('CRIAR JOGO', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/screens/team_stats_page.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../models/team_model.dart';
|
import '../models/team_model.dart';
|
||||||
import '../widgets/team_widgets.dart';
|
import '../widgets/team_widgets.dart';
|
||||||
@@ -24,7 +25,7 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- POPUP DE FILTROS (ESTILO DIALOG CENTRAL) ---
|
// --- POPUP DE FILTROS ---
|
||||||
void _showFilterDialog(BuildContext context) {
|
void _showFilterDialog(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -32,6 +33,7 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (context, setModalState) {
|
builder: (context, setModalState) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
backgroundColor: const Color(0xFF2C3E50), // 2. CORRIGIDO: Fundo escuro
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
title: Row(
|
title: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -116,7 +118,8 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
child: Text(
|
child: Text(
|
||||||
opt,
|
opt,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isSelected ? const Color(0xFFE74C3C) : Colors.black,
|
// 3. CORRIGIDO: Cor do texto (Branco se não selecionado, Vermelho se selecionado)
|
||||||
|
color: isSelected ? const Color(0xFFE74C3C) : Colors.white70,
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -177,46 +180,61 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
stream: controller.teamsStream,
|
stream: controller.teamsStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
var data = snapshot.data!;
|
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||||
|
return const Center(child: Text("Nenhuma equipa encontrada."));
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Filtro Temporada
|
var data = List<Map<String, dynamic>>.from(snapshot.data!);
|
||||||
|
|
||||||
|
// --- 1. FILTROS ---
|
||||||
if (_selectedSeason != 'Todas') {
|
if (_selectedSeason != 'Todas') {
|
||||||
data = data.where((t) => t['season'] == _selectedSeason).toList();
|
data = data.where((t) => t['season'] == _selectedSeason).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Filtro Pesquisa
|
|
||||||
if (_searchQuery.isNotEmpty) {
|
if (_searchQuery.isNotEmpty) {
|
||||||
data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList();
|
data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Ordenação (O controller já lida com favoritos, aqui aplicamos a manual)
|
// --- 2. ORDENAÇÃO (FAVORITOS PRIMEIRO) ---
|
||||||
if (_currentSort == 'Recentes') {
|
|
||||||
data.sort((a, b) => b['id'].compareTo(a['id']));
|
|
||||||
} else if (_currentSort == 'Nome') {
|
|
||||||
data.sort((a, b) => a['name'].toString().compareTo(b['name'].toString()));
|
|
||||||
} else if (_currentSort == 'Tamanho') {
|
|
||||||
data.sort((a, b) {
|
data.sort((a, b) {
|
||||||
int countA = TeamController.members.where((m) => m['team_id'] == a['id']).length;
|
// Apanhar o estado de favorito (tratando null como false)
|
||||||
int countB = TeamController.members.where((m) => m['team_id'] == b['id']).length;
|
bool favA = a['is_favorite'] ?? false;
|
||||||
return countB.compareTo(countA);
|
bool favB = b['is_favorite'] ?? false;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.isEmpty) {
|
// REGRA 1: Favoritos aparecem sempre primeiro
|
||||||
return const Center(child: Text("Nenhuma equipa encontrada."));
|
if (favA && !favB) return -1; // A sobe
|
||||||
|
if (!favA && favB) return 1; // B sobe
|
||||||
|
|
||||||
|
// REGRA 2: Se o estado de favorito for igual, aplica o filtro do utilizador
|
||||||
|
if (_currentSort == 'Nome') {
|
||||||
|
return a['name'].toString().compareTo(b['name'].toString());
|
||||||
|
} else { // Recentes
|
||||||
|
return (b['created_at'] ?? '').toString().compareTo((a['created_at'] ?? '').toString());
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
itemCount: data.length,
|
itemCount: data.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final team = Team.fromMap(data[index]);
|
final team = Team.fromMap(data[index]);
|
||||||
return TeamCard(
|
|
||||||
|
// Navegação para estatísticas
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: TeamCard(
|
||||||
team: team,
|
team: team,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onFavoriteTap: () => controller.toggleFavorite(team.id),
|
onFavoriteTap: () => controller.toggleFavorite(team.id, team.isFavorite),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -232,4 +250,5 @@ class _TeamsPageState extends State<TeamsPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,30 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:playmaker/controllers/team_controller.dart';
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import '../models/team_model.dart';
|
import '../models/team_model.dart';
|
||||||
import '../models/person_model.dart';
|
import '../models/person_model.dart';
|
||||||
import '../controllers/team_controllers.dart'; // Para aceder à lista estática de membros
|
// Se tiveres os widgets noutro ficheiro, importa-os.
|
||||||
|
// Caso contrário, podes colar as classes StatsHeader, etc. no fundo deste ficheiro.
|
||||||
|
import '../widgets/team_widgets.dart';
|
||||||
|
import '../widgets/stats_widgets.dart'; // Assumindo que criaste este ficheiro anteriormente
|
||||||
|
|
||||||
class TeamStatsPage extends StatelessWidget {
|
class TeamStatsPage extends StatefulWidget {
|
||||||
final Team team;
|
final Team team;
|
||||||
|
|
||||||
|
const TeamStatsPage({super.key, required this.team});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TeamStatsPage> createState() => _TeamStatsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TeamStatsPageState extends State<TeamStatsPage> {
|
||||||
|
// Instancia o controlador local
|
||||||
final StatsController _controller = StatsController();
|
final StatsController _controller = StatsController();
|
||||||
|
|
||||||
TeamStatsPage({super.key, required this.team});
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -17,10 +32,13 @@ class TeamStatsPage extends StatelessWidget {
|
|||||||
backgroundColor: const Color(0xFFF5F7FA),
|
backgroundColor: const Color(0xFFF5F7FA),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildLocalHeader(context),
|
// Header (Widget que criámos antes)
|
||||||
|
StatsHeader(team: widget.team),
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: StreamBuilder<List<Person>>(
|
child: StreamBuilder<List<Person>>(
|
||||||
stream: _controller.getMembers(team.id),
|
// LÊ DA LISTA LOCAL
|
||||||
|
stream: _controller.getMembers(widget.team.id),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
@@ -35,21 +53,36 @@ class TeamStatsPage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildLocalSummaryCard(members.length),
|
// Resumo
|
||||||
|
StatsSummaryCard(total: members.length),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Secção Treinadores
|
||||||
if (coaches.isNotEmpty) ...[
|
if (coaches.isNotEmpty) ...[
|
||||||
_buildSectionTitle("Treinadores"),
|
const StatsSectionTitle(title: "Treinadores"),
|
||||||
...coaches.map((c) => _buildPersonCard(context, c, isCoach: true)),
|
...coaches.map((c) => PersonCard(
|
||||||
|
person: c,
|
||||||
|
isCoach: true,
|
||||||
|
onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, c),
|
||||||
|
onDelete: () => _confirmDelete(context, c),
|
||||||
|
)),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
],
|
],
|
||||||
_buildSectionTitle("Jogadores"),
|
|
||||||
|
// Secção Jogadores
|
||||||
|
const StatsSectionTitle(title: "Jogadores"),
|
||||||
if (players.isEmpty)
|
if (players.isEmpty)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(top: 20),
|
padding: EdgeInsets.only(top: 20),
|
||||||
child: Text("Nenhum jogador adicionado.", style: TextStyle(color: Colors.grey)),
|
child: Text("Nenhum jogador adicionado.", style: TextStyle(color: Colors.grey)),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
...players.map((p) => _buildPersonCard(context, p, isCoach: false)),
|
...players.map((p) => PersonCard(
|
||||||
|
person: p,
|
||||||
|
isCoach: false,
|
||||||
|
onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, p),
|
||||||
|
onDelete: () => _confirmDelete(context, p),
|
||||||
|
)),
|
||||||
const SizedBox(height: 80),
|
const SizedBox(height: 80),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -60,100 +93,14 @@ class TeamStatsPage extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () => _controller.showAddPersonDialog(context, team.id),
|
heroTag: 'player_fab',
|
||||||
|
onPressed: () => _controller.showAddPersonDialog(context, widget.team.id),
|
||||||
backgroundColor: const Color(0xFF00C853),
|
backgroundColor: const Color(0xFF00C853),
|
||||||
child: const Icon(Icons.add, color: Colors.white),
|
child: const Icon(Icons.add, color: Colors.white),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WIDGETS QUE ESTAVAM EM STATS_WIDGETS ---
|
|
||||||
|
|
||||||
Widget _buildLocalHeader(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFF2C3E50),
|
|
||||||
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(30), bottomRight: Radius.circular(30)),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
CircleAvatar(
|
|
||||||
backgroundColor: Colors.white24,
|
|
||||||
child: Text(team.imageUrl.isNotEmpty && !team.imageUrl.startsWith('http') ? team.imageUrl : "🏀"),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 15),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(team.name, style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
|
|
||||||
Text(team.season, style: const TextStyle(color: Colors.white70, fontSize: 14)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildLocalSummaryCard(int total) {
|
|
||||||
return Card(
|
|
||||||
elevation: 4,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
gradient: LinearGradient(colors: [Colors.blue.shade700, Colors.blue.shade400]),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
const Text("Total de Membros", style: TextStyle(color: Colors.white, fontSize: 16)),
|
|
||||||
Text("$total", style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPersonCard(BuildContext context, Person person, {required bool isCoach}) {
|
|
||||||
return Card(
|
|
||||||
margin: const EdgeInsets.only(top: 12),
|
|
||||||
elevation: 2,
|
|
||||||
color: isCoach ? const Color(0xFFFFF9C4) : Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
|
||||||
child: ListTile(
|
|
||||||
leading: isCoach
|
|
||||||
? const CircleAvatar(backgroundColor: Colors.orange, child: Icon(Icons.person, color: Colors.white))
|
|
||||||
: Container(
|
|
||||||
width: 45, height: 45,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(10)),
|
|
||||||
child: Text(person.number, style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 16)),
|
|
||||||
),
|
|
||||||
title: Text(person.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit_outlined, color: Colors.blue),
|
|
||||||
onPressed: () => _controller.showEditPersonDialog(context, team.id, person),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
|
||||||
onPressed: () => _confirmDelete(context, person),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _confirmDelete(BuildContext context, Person person) {
|
void _confirmDelete(BuildContext context, Person person) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -164,7 +111,7 @@ class TeamStatsPage extends StatelessWidget {
|
|||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("Cancelar")),
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text("Cancelar")),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_controller.deletePerson(team.id, person.id);
|
_controller.deletePerson(widget.team.id, person.id);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: const Text("Eliminar", style: TextStyle(color: Colors.red)),
|
child: const Text("Eliminar", style: TextStyle(color: Colors.red)),
|
||||||
@@ -173,41 +120,47 @@ class TeamStatsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSectionTitle(String title) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF2C3E50))),
|
|
||||||
const Divider(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- CONTROLLER LOCAL (SEM SUPABASE) ---
|
// --- CONTROLLER LOCAL (SEM SUPABASE) ---
|
||||||
|
|
||||||
class StatsController {
|
class StatsController {
|
||||||
final _streamController = StreamController<List<Person>>.broadcast();
|
// Base de dados simulada na memória (Estática para não perder dados ao mudar de ecrã)
|
||||||
|
static final List<Map<String, dynamic>> _mockMembers = [
|
||||||
|
// Podes deixar vazio ou meter dados de teste
|
||||||
|
// {'id': '1', 'team_id': 'exemplo', 'name': 'Mourinho', 'type': 'Treinador', 'number': null},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Stream para atualizar a UI automaticamente
|
||||||
|
final StreamController<List<Person>> _streamController = StreamController<List<Person>>.broadcast();
|
||||||
|
|
||||||
|
// 1. LER (Filtra a lista local)
|
||||||
Stream<List<Person>> getMembers(String teamId) {
|
Stream<List<Person>> getMembers(String teamId) {
|
||||||
_emitMembers(teamId);
|
// Pequeno delay para garantir que o Stream ouve a primeira emissão
|
||||||
|
Future.delayed(Duration.zero, () => _emitMembers(teamId));
|
||||||
return _streamController.stream;
|
return _streamController.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Função auxiliar para atualizar quem está a ouvir
|
||||||
void _emitMembers(String teamId) {
|
void _emitMembers(String teamId) {
|
||||||
final list = TeamController.members
|
final filtered = _mockMembers
|
||||||
.where((m) => m['team_id'] == teamId)
|
.where((m) => m['team_id'] == teamId)
|
||||||
.map((json) => Person.fromMap(json))
|
.map((m) => Person.fromMap(m))
|
||||||
.toList();
|
.toList();
|
||||||
_streamController.add(list);
|
|
||||||
|
// Ordenar por nome
|
||||||
|
filtered.sort((a, b) => a.name.compareTo(b.name));
|
||||||
|
|
||||||
|
_streamController.add(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. APAGAR LOCALMENTE
|
||||||
void deletePerson(String teamId, String personId) {
|
void deletePerson(String teamId, String personId) {
|
||||||
TeamController.members.removeWhere((m) => m['id'] == personId);
|
_mockMembers.removeWhere((m) => m['id'] == personId);
|
||||||
_emitMembers(teamId);
|
_emitMembers(teamId); // Atualiza o ecrã
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. DIÁLOGOS
|
||||||
void showAddPersonDialog(BuildContext context, String teamId) {
|
void showAddPersonDialog(BuildContext context, String teamId) {
|
||||||
_showForm(context, teamId: teamId);
|
_showForm(context, teamId: teamId);
|
||||||
}
|
}
|
||||||
@@ -220,7 +173,7 @@ class StatsController {
|
|||||||
final isEdit = person != null;
|
final isEdit = person != null;
|
||||||
final nameCtrl = TextEditingController(text: person?.name ?? '');
|
final nameCtrl = TextEditingController(text: person?.name ?? '');
|
||||||
final numCtrl = TextEditingController(text: person?.number ?? '');
|
final numCtrl = TextEditingController(text: person?.number ?? '');
|
||||||
String type = person?.type ?? 'Jogador';
|
String selectedType = person?.type ?? 'Jogador';
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -230,41 +183,80 @@ class StatsController {
|
|||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextField(controller: nameCtrl, decoration: const InputDecoration(labelText: "Nome")),
|
TextField(
|
||||||
DropdownButton<String>(
|
controller: nameCtrl,
|
||||||
value: type,
|
decoration: const InputDecoration(labelText: "Nome"),
|
||||||
isExpanded: true,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
items: ["Jogador", "Treinador"].map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(),
|
|
||||||
onChanged: (v) => setState(() => type = v!),
|
|
||||||
),
|
),
|
||||||
if (type == "Jogador")
|
const SizedBox(height: 10),
|
||||||
TextField(controller: numCtrl, decoration: const InputDecoration(labelText: "Número"), keyboardType: TextInputType.number),
|
DropdownButtonFormField<String>(
|
||||||
|
value: selectedType,
|
||||||
|
decoration: const InputDecoration(labelText: "Função"),
|
||||||
|
items: ["Jogador", "Treinador"].map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(),
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) setState(() => selectedType = v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (selectedType == "Jogador") ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
TextField(
|
||||||
|
controller: numCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: "Número da Camisola"),
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Sair")),
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
child: const Text("Cancelar")
|
||||||
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (nameCtrl.text.trim().isEmpty) return;
|
||||||
|
|
||||||
|
String? numeroFinal = (selectedType == "Treinador")
|
||||||
|
? null
|
||||||
|
: (numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim());
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
final idx = TeamController.members.indexWhere((m) => m['id'] == person.id);
|
// ATUALIZAR NA LISTA LOCAL
|
||||||
TeamController.members[idx] = {'id': person.id, 'team_id': teamId, 'name': nameCtrl.text, 'type': type, 'number': numCtrl.text};
|
final index = _mockMembers.indexWhere((m) => m['id'] == person!.id);
|
||||||
} else {
|
if (index != -1) {
|
||||||
TeamController.members.add({
|
_mockMembers[index] = {
|
||||||
'id': DateTime.now().toString(),
|
'id': person!.id,
|
||||||
'team_id': teamId,
|
'team_id': teamId,
|
||||||
'name': nameCtrl.text,
|
'name': nameCtrl.text.trim(),
|
||||||
'type': type,
|
'type': selectedType,
|
||||||
'number': numCtrl.text
|
'number': numeroFinal,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// CRIAR NA LISTA LOCAL (Gera ID com a data atual)
|
||||||
|
_mockMembers.add({
|
||||||
|
'id': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
|
'team_id': teamId,
|
||||||
|
'name': nameCtrl.text.trim(),
|
||||||
|
'type': selectedType,
|
||||||
|
'number': numeroFinal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Atualiza a UI
|
||||||
_emitMembers(teamId);
|
_emitMembers(teamId);
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
},
|
},
|
||||||
child: const Text("Guardar"),
|
child: const Text("Guardar", style: TextStyle(color: Colors.white)),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_streamController.close();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
220
lib/widgets/game_widgets.dart
Normal file
220
lib/widgets/game_widgets.dart
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/pages/PlacarPage.dart';
|
||||||
|
import '../controllers/team_controller.dart';
|
||||||
|
|
||||||
|
// --- CARD DE EXIBIÇÃO DO JOGO ---
|
||||||
|
class GameResultCard extends StatelessWidget {
|
||||||
|
final String gameId; // Adicionado para identificar o jogo no retorno
|
||||||
|
final String myTeam, opponentTeam, myScore, opponentScore, status, season;
|
||||||
|
|
||||||
|
const GameResultCard({
|
||||||
|
super.key,
|
||||||
|
required this.gameId,
|
||||||
|
required this.myTeam,
|
||||||
|
required this.opponentTeam,
|
||||||
|
required this.myScore,
|
||||||
|
required this.opponentScore,
|
||||||
|
required this.status,
|
||||||
|
required this.season,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTeamInfo(String name, Color color) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
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)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScoreCenter(BuildContext context, String id) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_scoreBox(myScore, Colors.green),
|
||||||
|
const Text(" : ", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||||
|
_scoreBox(opponentScore, Colors.grey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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
|
||||||
|
},
|
||||||
|
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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFE74C3C).withOpacity(0.1),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(status, style: const TextStyle(fontSize: 10, color: Colors.blue, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _scoreBox(String pts, Color c) => Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
decoration: BoxDecoration(color: c, borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Text(pts, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- POPUP DE CRIAÇÃO ---
|
||||||
|
class CreateGameDialogManual extends StatefulWidget {
|
||||||
|
final TeamController controller;
|
||||||
|
final Function(String, String, String) onConfirm;
|
||||||
|
|
||||||
|
const CreateGameDialogManual({super.key, required this.controller, required this.onConfirm});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateGameDialogManual> createState() => _CreateGameDialogManualState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateGameDialogManualState extends State<CreateGameDialogManual> {
|
||||||
|
late TextEditingController _seasonController;
|
||||||
|
String _myTeamName = "";
|
||||||
|
String _opponentName = "";
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_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)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
title: const Text('Configurar Partida', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _seasonController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Temporada',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.calendar_today, size: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
_buildSearch(label: "Minha Equipa", onSelect: (v) => _myTeamName = v),
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('CRIAR', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearch({required String label, required Function(String) onSelect}) {
|
||||||
|
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||||
|
stream: widget.controller.teamsStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
List<String> teamList = snapshot.hasData ? snapshot.data!.map((t) => t['name'].toString()).toList() : [];
|
||||||
|
return Autocomplete<String>(
|
||||||
|
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()
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
lib/widgets/stats_widgets.dart
Normal file
140
lib/widgets/stats_widgets.dart
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../models/team_model.dart';
|
||||||
|
import '../models/person_model.dart';
|
||||||
|
|
||||||
|
// --- CABEÇALHO ---
|
||||||
|
class StatsHeader extends StatelessWidget {
|
||||||
|
final Team team;
|
||||||
|
|
||||||
|
const StatsHeader({super.key, required this.team});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFF2C3E50),
|
||||||
|
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(30), bottomRight: Radius.circular(30)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor: Colors.white24,
|
||||||
|
child: Text(team.imageUrl.isNotEmpty ? "📷" : "🛡️"),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(team.name, style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
|
Text(team.season, style: const TextStyle(color: Colors.white70, fontSize: 14)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CARD DE RESUMO ---
|
||||||
|
class StatsSummaryCard extends StatelessWidget {
|
||||||
|
final int total;
|
||||||
|
|
||||||
|
const StatsSummaryCard({super.key, required this.total});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
elevation: 4,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
gradient: LinearGradient(colors: [Colors.blue.shade700, Colors.blue.shade400]),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text("Total de Membros", style: TextStyle(color: Colors.white, fontSize: 16)),
|
||||||
|
Text("$total", style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TÍTULO DE SECÇÃO ---
|
||||||
|
class StatsSectionTitle extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
const StatsSectionTitle({super.key, required this.title});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF2C3E50))),
|
||||||
|
const Divider(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CARD DA PESSOA (JOGADOR/TREINADOR) ---
|
||||||
|
class PersonCard extends StatelessWidget {
|
||||||
|
final Person person;
|
||||||
|
final bool isCoach;
|
||||||
|
final VoidCallback onEdit;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
|
||||||
|
const PersonCard({
|
||||||
|
super.key,
|
||||||
|
required this.person,
|
||||||
|
required this.isCoach,
|
||||||
|
required this.onEdit,
|
||||||
|
required this.onDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(top: 12),
|
||||||
|
elevation: 2,
|
||||||
|
color: isCoach ? const Color(0xFFFFF9C4) : Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||||
|
child: ListTile(
|
||||||
|
leading: isCoach
|
||||||
|
? const CircleAvatar(backgroundColor: Colors.orange, child: Icon(Icons.person, color: Colors.white))
|
||||||
|
: Container(
|
||||||
|
width: 45,
|
||||||
|
height: 45,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: Text(person.number, style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
|
),
|
||||||
|
title: Text(person.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit_outlined, color: Colors.blue),
|
||||||
|
onPressed: onEdit,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||||
|
onPressed: onDelete,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:playmaker/screens/team_stats_page.dart';
|
||||||
import '../models/team_model.dart';
|
import '../models/team_model.dart';
|
||||||
import '../controllers/team_controller.dart';
|
import '../controllers/team_controller.dart';
|
||||||
import '../screens/team_stats_page.dart';
|
|
||||||
class TeamCard extends StatelessWidget {
|
class TeamCard extends StatelessWidget {
|
||||||
final Team team;
|
final Team team;
|
||||||
final TeamController controller;
|
final TeamController controller;
|
||||||
@@ -24,10 +25,10 @@ class TeamCard extends StatelessWidget {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
|
||||||
|
// --- 1. IMAGEM + FAVORITO ---
|
||||||
leading: Stack(
|
leading: Stack(
|
||||||
clipBehavior: Clip.none, // Permite que a estrela flutue ligeiramente fora do círculo
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
// 1. IMAGEM DA EQUIPA
|
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 28,
|
radius: 28,
|
||||||
backgroundColor: Colors.grey[200],
|
backgroundColor: Colors.grey[200],
|
||||||
@@ -41,16 +42,13 @@ class TeamCard extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 2. BOTÃO DA ESTRELA (Favorito)
|
|
||||||
Positioned(
|
Positioned(
|
||||||
left: -15, // Posiciona à esquerda da imagem
|
left: -15,
|
||||||
top: -10,
|
top: -10,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
// O segredo está em colocar o shadow dentro do Icon:
|
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
team.isFavorite ? Icons.star : Icons.star_border,
|
team.isFavorite ? Icons.star : Icons.star_border,
|
||||||
color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1), // Transparente se não favorito
|
color: team.isFavorite ? Colors.amber : Colors.black.withOpacity(0.1),
|
||||||
size: 28,
|
size: 28,
|
||||||
shadows: [
|
shadows: [
|
||||||
Shadow(
|
Shadow(
|
||||||
@@ -65,13 +63,13 @@ class TeamCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// --- NOME DA EQUIPA ---
|
// --- 2. TÍTULO ---
|
||||||
title: Text(
|
title: Text(
|
||||||
team.name,
|
team.name,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
),
|
||||||
|
|
||||||
// --- SUBTÍTULO (CONTAGEM E TEMPORADA) ---
|
// --- 3. SUBTÍTULO (Contagem + Época) ---
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.only(top: 6.0),
|
padding: const EdgeInsets.only(top: 6.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -102,9 +100,9 @@ class TeamCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// --- BOTÕES DE ACÇÃO À DIREITA ---
|
// --- 4. BOTÕES (Estatísticas e Apagar) ---
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
width: 80,
|
width: 96, // Aumentei um pouco para caberem bem os dois botões
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
@@ -112,6 +110,7 @@ class TeamCard extends StatelessWidget {
|
|||||||
tooltip: 'Ver Estatísticas',
|
tooltip: 'Ver Estatísticas',
|
||||||
icon: const Icon(Icons.bar_chart_rounded, color: Colors.blue),
|
icon: const Icon(Icons.bar_chart_rounded, color: Colors.blue),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
// CORRIGIDO: Agora chama a classe TeamStatsPage corretamente
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -131,6 +130,8 @@ class TeamCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Função de confirmação de exclusão
|
||||||
void _confirmDelete(BuildContext context) {
|
void _confirmDelete(BuildContext context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -140,21 +141,22 @@ class TeamCard extends StatelessWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Cancelar')
|
child: const Text('Cancelar'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.deleteTeam(team.id);
|
controller.deleteTeam(team.id);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: const Text('Eliminar', style: TextStyle(color: Colors.red))
|
child: const Text('Eliminar', style: TextStyle(color: Colors.red)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- DIALOG DE CRIAÇÃO ---
|
||||||
class CreateTeamDialog extends StatefulWidget {
|
class CreateTeamDialog extends StatefulWidget {
|
||||||
final Function(String name, String season, String imageUrl) onConfirm;
|
final Function(String name, String season, String imageUrl) onConfirm;
|
||||||
|
|
||||||
@@ -206,16 +208,16 @@ class _CreateTeamDialogState extends State<CreateTeamDialog> {
|
|||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancelar')),
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancelar')),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C)),
|
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C)),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_nameController.text.trim().isNotEmpty) {
|
if (_nameController.text.trim().isNotEmpty) {
|
||||||
widget.onConfirm(
|
widget.onConfirm(
|
||||||
_nameController.text.trim(),
|
_nameController.text.trim(),
|
||||||
_selectedSeason,
|
_selectedSeason,
|
||||||
_imageController.text.trim()
|
_imageController.text.trim(),
|
||||||
);
|
);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('Criar', style: TextStyle(color: Colors.white)),
|
child: const Text('Criar', style: TextStyle(color: Colors.white)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||||
|
|||||||
42
macos/Podfile
Normal file
42
macos/Podfile
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
platform :osx, '10.15'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_macos_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
|
||||||
|
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_macos_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user