ndk
This commit is contained in:
@@ -8,8 +8,8 @@ plugins {
|
||||
android {
|
||||
namespace = "com.example.playmaker"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
//ndkVersion = flutter.ndkVersion
|
||||
ndkVersion = "26.1.10909125"
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -39,12 +39,14 @@ android {
|
||||
}
|
||||
|
||||
// Adicione no FINAL do arquivo:
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
|
||||
dependencies {
|
||||
// Dependências do Firebase que você vai usar
|
||||
implementation 'com.google.firebase:firebase-analytics'
|
||||
// Adicione outras conforme necessário
|
||||
// Usa parênteses e aspas duplas
|
||||
implementation("com.google.firebase:firebase-analytics")
|
||||
|
||||
// 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 {
|
||||
repositories {
|
||||
google()
|
||||
@@ -15,16 +26,11 @@ subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
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("com.android.application") version "8.9.1" apply false
|
||||
// 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
|
||||
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"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.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 {
|
||||
final SupabaseClient _supabase = Supabase.instance.client;
|
||||
|
||||
// --- 1. LER MEMBROS (STREAM EM TEMPO REAL) ---
|
||||
// 1. LER
|
||||
Stream<List<Person>> getMembers(String teamId) {
|
||||
return _supabase
|
||||
.from('members') // Nome da tua tabela no Supabase
|
||||
.from('members')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('team_id', teamId)
|
||||
.order('name', ascending: true)
|
||||
.map((data) => data.map((json) => Person.fromMap(json)).toList());
|
||||
}
|
||||
|
||||
// --- 2. APAGAR ---
|
||||
Future<void> deletePerson(String teamId, String personId) async {
|
||||
// 2. APAGAR
|
||||
Future<void> deletePerson(String personId) async {
|
||||
try {
|
||||
await _supabase.from('members').delete().eq('id', personId);
|
||||
} catch (e) {
|
||||
@@ -24,112 +24,135 @@ class StatsController {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. DIÁLOGOS DE ADICIONAR / EDITAR ---
|
||||
|
||||
// Abrir diálogo para criar novo
|
||||
// 3. DIÁLOGOS
|
||||
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) {
|
||||
_showPersonForm(context, teamId: teamId, person: person);
|
||||
_showForm(context, teamId: teamId, person: person);
|
||||
}
|
||||
|
||||
// Lógica interna do formulário
|
||||
void _showPersonForm(BuildContext context, {required String teamId, Person? person}) {
|
||||
final isEditing = person != null;
|
||||
// --- O POPUP ESTÁ AQUI ---
|
||||
void _showForm(BuildContext context, {required String teamId, Person? person}) {
|
||||
final isEdit = person != null;
|
||||
final nameCtrl = TextEditingController(text: person?.name ?? '');
|
||||
final numCtrl = TextEditingController(text: person?.number ?? '');
|
||||
|
||||
// Controladores de texto
|
||||
final nameController = TextEditingController(text: person?.name ?? '');
|
||||
final numberController = TextEditingController(text: person?.number ?? '');
|
||||
|
||||
// Variável para o Dropdown (Valor inicial)
|
||||
// Define o valor inicial
|
||||
String selectedType = person?.type ?? 'Jogador';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
// StatefulBuilder serve para atualizar o Dropdown DENTRO do diálogo
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(isEditing ? 'Editar Membro' : 'Novo Membro'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Nome
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(labelText: 'Nome'),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Tipo (Jogador vs Treinador)
|
||||
DropdownButtonFormField<String>(
|
||||
value: selectedType,
|
||||
decoration: const InputDecoration(labelText: 'Função'),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'Jogador', child: Text('Jogador')),
|
||||
DropdownMenuItem(value: 'Treinador', child: Text('Treinador')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() => selectedType = value);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Número (Só mostramos se for Jogador)
|
||||
if (selectedType == 'Jogador')
|
||||
TextField(
|
||||
controller: numberController,
|
||||
decoration: const InputDecoration(labelText: 'Número da Camisola'),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
],
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setState) => AlertDialog(
|
||||
title: Text(isEdit ? "Editar" : "Adicionar"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// NOME
|
||||
TextField(
|
||||
controller: nameCtrl,
|
||||
decoration: const InputDecoration(labelText: "Nome"),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)),
|
||||
onPressed: () async {
|
||||
if (nameController.text.trim().isEmpty) return;
|
||||
const SizedBox(height: 10),
|
||||
|
||||
final name = nameController.text.trim();
|
||||
final number = numberController.text.trim();
|
||||
// FUNÇÃO
|
||||
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 (isEditing) {
|
||||
// ATUALIZAR
|
||||
await _supabase.from('members').update({
|
||||
'name': name,
|
||||
'type': selectedType,
|
||||
'number': number,
|
||||
}).eq('id', person!.id);
|
||||
} else {
|
||||
// CRIAR NOVO
|
||||
await _supabase.from('members').insert({
|
||||
'team_id': teamId,
|
||||
'name': name,
|
||||
'type': selectedType,
|
||||
'number': number,
|
||||
});
|
||||
}
|
||||
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
child: Text(isEditing ? 'Guardar' : 'Adicionar', style: const TextStyle(color: Colors.white)),
|
||||
// NÚMERO (Só aparece se for Jogador)
|
||||
if (selectedType == "Jogador") ...[
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: numCtrl,
|
||||
decoration: const InputDecoration(labelText: "Número da Camisola"),
|
||||
keyboardType: TextInputType.text, // Aceita texto para evitar erros
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text("Cancelar")
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)),
|
||||
onPressed: () async {
|
||||
print("--- 1. CLICOU EM GUARDAR ---");
|
||||
|
||||
// Validação Simples
|
||||
if (nameCtrl.text.trim().isEmpty) {
|
||||
print("ERRO: Nome vazio");
|
||||
return;
|
||||
}
|
||||
|
||||
// Lógica do Número:
|
||||
// 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({
|
||||
'name': nameCtrl.text.trim(),
|
||||
'type': selectedType,
|
||||
'number': numeroFinal,
|
||||
}).eq('id', person!.id);
|
||||
} else {
|
||||
await _supabase.from('members').insert({
|
||||
'team_id': teamId, // Verifica se este teamId é válido!
|
||||
'name': nameCtrl.text.trim(),
|
||||
'type': selectedType,
|
||||
'number': numeroFinal,
|
||||
});
|
||||
}
|
||||
|
||||
print("--- 3. SUCESSO! FECHANDO DIÁLOGO ---");
|
||||
if (ctx.mounted) Navigator.pop(ctx);
|
||||
|
||||
} 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 {
|
||||
// --- BASE DE DADOS LOCAL (Listas Estáticas) ---
|
||||
// Mantemos estático para que os dados persistam entre navegações de ecrãs
|
||||
static final List<Map<String, dynamic>> _teams = [];
|
||||
static final List<Map<String, dynamic>> _members = [];
|
||||
// Instância do cliente Supabase
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
static List<Map<String, dynamic>> get members => _members;
|
||||
|
||||
// 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
|
||||
// 1. STREAM (Realtime)
|
||||
// Adicionei o .map() no final para garantir que o Dart entende que é uma List<Map>
|
||||
Stream<List<Map<String, dynamic>>> get teamsStream {
|
||||
_notifyListeners();
|
||||
return _streamController.stream;
|
||||
return _supabase
|
||||
.from('teams')
|
||||
.stream(primaryKey: ['id'])
|
||||
.order('name', ascending: true)
|
||||
.map((data) => List<Map<String, dynamic>>.from(data));
|
||||
}
|
||||
|
||||
// 2. CRIAR
|
||||
Future<void> createTeam(String name, String season, String imageUrl) async {
|
||||
await Future.delayed(const Duration(milliseconds: 100)); // Simula latência
|
||||
final newTeam = {
|
||||
'id': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'name': name,
|
||||
'season': season,
|
||||
'image_url': imageUrl,
|
||||
'is_favorite': false, // Inicializa sempre como falso
|
||||
};
|
||||
_teams.add(newTeam);
|
||||
_notifyListeners();
|
||||
// Alterei imageUrl para String? (pode ser nulo) para evitar erros se não houver imagem
|
||||
Future<void> createTeam(String name, String season, String? imageUrl) async {
|
||||
try {
|
||||
await _supabase.from('teams').insert({
|
||||
'name': name,
|
||||
'season': season,
|
||||
'image_url': imageUrl,
|
||||
'is_favorite': false,
|
||||
});
|
||||
print("✅ Equipa guardada no Supabase!");
|
||||
} catch (e) {
|
||||
print("❌ Erro ao criar: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. ELIMINAR
|
||||
Future<void> deleteTeam(String id) async {
|
||||
_teams.removeWhere((team) => team['id'] == id);
|
||||
_members.removeWhere((member) => member['team_id'] == id);
|
||||
_notifyListeners();
|
||||
try {
|
||||
await _supabase.from('teams').delete().eq('id', id);
|
||||
} catch (e) {
|
||||
print("❌ Erro ao eliminar: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. FAVORITAR
|
||||
Future<void> toggleFavorite(String teamId) async {
|
||||
final index = _teams.indexWhere((t) => t['id'] == teamId);
|
||||
if (index != -1) {
|
||||
// Inverte o valor booleano (trata null como false)
|
||||
final bool currentStatus = _teams[index]['is_favorite'] ?? false;
|
||||
_teams[index]['is_favorite'] = !currentStatus;
|
||||
_notifyListeners();
|
||||
Future<void> toggleFavorite(String teamId, bool currentStatus) async {
|
||||
try {
|
||||
await _supabase
|
||||
.from('teams')
|
||||
.update({'is_favorite': !currentStatus}) // Inverte o valor
|
||||
.eq('id', teamId);
|
||||
} catch (e) {
|
||||
print("❌ Erro ao favoritar: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. CONTAR JOGADORES
|
||||
// CORRIGIDO: A sintaxe antiga dava erro. O método .count() é o correto agora.
|
||||
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)
|
||||
void _notifyListeners() {
|
||||
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();
|
||||
}
|
||||
// Mantemos o dispose vazio para não quebrar a chamada na TeamsPage
|
||||
void dispose() {}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
|
||||
// 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) {
|
||||
return Team(
|
||||
id: map['id'] ?? '',
|
||||
id: map['id']?.toString() ?? '',
|
||||
name: map['name'] ?? '',
|
||||
season: map['season'] ?? '',
|
||||
imageUrl: map['image_url'] ?? '',
|
||||
@@ -24,10 +24,8 @@ class Team {
|
||||
);
|
||||
}
|
||||
|
||||
// Converte de Objeto para Mapa (para guardar na lista)
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'season': season,
|
||||
'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 '../controllers/game_controller.dart';
|
||||
import '../controllers/team_controller.dart';
|
||||
|
||||
import '../models/game_model.dart';
|
||||
import '../widgets/game_widgets.dart';
|
||||
|
||||
class GamePage extends StatefulWidget {
|
||||
const GamePage({super.key});
|
||||
@@ -10,103 +12,17 @@ class GamePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GamePageState extends State<GamePage> {
|
||||
final TeamController controller = TeamController();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
String _selectedSeasonFilter = 'Todas';
|
||||
String _currentSort = 'Recentes';
|
||||
String _searchQuery = '';
|
||||
// Criamos as instâncias dos controllers
|
||||
final GameController gameController = GameController();
|
||||
final TeamController teamController = TeamController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
// É importante fechar os streams quando a página sai da memória
|
||||
gameController.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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -115,194 +31,55 @@ class _GamePageState extends State<GamePage> {
|
||||
title: const Text("Jogos", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_list, color: Color(0xFFE74C3C)),
|
||||
onPressed: () => _showFilterDialog(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (v) => setState(() => _searchQuery = v.toLowerCase()),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Pesquisar jogo...',
|
||||
prefixIcon: const Icon(Icons.search, color: Color(0xFFE74C3C)),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Expanded(child: Center(child: Text("Nenhum jogo registado."))),
|
||||
],
|
||||
body: StreamBuilder<List<Game>>(
|
||||
stream: gameController.gamesStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return const Center(child: Text("Nenhum jogo registado."));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final game = snapshot.data![index];
|
||||
|
||||
// ATUALIZADO: Passamos o gameId para o card
|
||||
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(
|
||||
onPressed: () => _showCreateGameDialog(context),
|
||||
backgroundColor: const Color(0xFFE74C3C),
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
onPressed: () => _showCreateDialog(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
void _showCreateDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => CreateGameDialogManual(
|
||||
controller: teamController,
|
||||
onConfirm: (my, opp, sea) {
|
||||
gameController.addGame(my, opp, sea);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:playmaker/screens/team_stats_page.dart';
|
||||
import '../controllers/team_controller.dart';
|
||||
import '../models/team_model.dart';
|
||||
import '../widgets/team_widgets.dart';
|
||||
@@ -24,7 +25,7 @@ class _TeamsPageState extends State<TeamsPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- POPUP DE FILTROS (ESTILO DIALOG CENTRAL) ---
|
||||
// --- POPUP DE FILTROS ---
|
||||
void _showFilterDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -32,6 +33,7 @@ class _TeamsPageState extends State<TeamsPage> {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return AlertDialog(
|
||||
backgroundColor: const Color(0xFF2C3E50), // 2. CORRIGIDO: Fundo escuro
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -116,7 +118,8 @@ class _TeamsPageState extends State<TeamsPage> {
|
||||
child: Text(
|
||||
opt,
|
||||
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,
|
||||
),
|
||||
),
|
||||
@@ -177,46 +180,61 @@ class _TeamsPageState extends State<TeamsPage> {
|
||||
return StreamBuilder<List<Map<String, dynamic>>>(
|
||||
stream: controller.teamsStream,
|
||||
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') {
|
||||
data = data.where((t) => t['season'] == _selectedSeason).toList();
|
||||
}
|
||||
|
||||
// 2. Filtro Pesquisa
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
data = data.where((t) => t['name'].toString().toLowerCase().contains(_searchQuery)).toList();
|
||||
}
|
||||
|
||||
// 3. Ordenação (O controller já lida com favoritos, aqui aplicamos a manual)
|
||||
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) {
|
||||
int countA = TeamController.members.where((m) => m['team_id'] == a['id']).length;
|
||||
int countB = TeamController.members.where((m) => m['team_id'] == b['id']).length;
|
||||
return countB.compareTo(countA);
|
||||
});
|
||||
}
|
||||
// --- 2. ORDENAÇÃO (FAVORITOS PRIMEIRO) ---
|
||||
data.sort((a, b) {
|
||||
// Apanhar o estado de favorito (tratando null como false)
|
||||
bool favA = a['is_favorite'] ?? false;
|
||||
bool favB = b['is_favorite'] ?? false;
|
||||
|
||||
if (data.isEmpty) {
|
||||
return const Center(child: Text("Nenhuma equipa encontrada."));
|
||||
}
|
||||
// REGRA 1: Favoritos aparecem sempre primeiro
|
||||
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(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final team = Team.fromMap(data[index]);
|
||||
return TeamCard(
|
||||
team: team,
|
||||
controller: controller,
|
||||
onFavoriteTap: () => controller.toggleFavorite(team.id),
|
||||
|
||||
// Navegação para estatísticas
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => TeamStatsPage(team: team)),
|
||||
);
|
||||
},
|
||||
child: TeamCard(
|
||||
team: team,
|
||||
controller: controller,
|
||||
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 'package:flutter/material.dart';
|
||||
import '../models/team_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;
|
||||
|
||||
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();
|
||||
|
||||
TeamStatsPage({super.key, required this.team});
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -17,10 +32,13 @@ class TeamStatsPage extends StatelessWidget {
|
||||
backgroundColor: const Color(0xFFF5F7FA),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildLocalHeader(context),
|
||||
// Header (Widget que criámos antes)
|
||||
StatsHeader(team: widget.team),
|
||||
|
||||
Expanded(
|
||||
child: StreamBuilder<List<Person>>(
|
||||
stream: _controller.getMembers(team.id),
|
||||
// LÊ DA LISTA LOCAL
|
||||
stream: _controller.getMembers(widget.team.id),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
@@ -35,21 +53,36 @@ class TeamStatsPage extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildLocalSummaryCard(members.length),
|
||||
// Resumo
|
||||
StatsSummaryCard(total: members.length),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Secção Treinadores
|
||||
if (coaches.isNotEmpty) ...[
|
||||
_buildSectionTitle("Treinadores"),
|
||||
...coaches.map((c) => _buildPersonCard(context, c, isCoach: true)),
|
||||
const StatsSectionTitle(title: "Treinadores"),
|
||||
...coaches.map((c) => PersonCard(
|
||||
person: c,
|
||||
isCoach: true,
|
||||
onEdit: () => _controller.showEditPersonDialog(context, widget.team.id, c),
|
||||
onDelete: () => _confirmDelete(context, c),
|
||||
)),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
_buildSectionTitle("Jogadores"),
|
||||
|
||||
// Secção Jogadores
|
||||
const StatsSectionTitle(title: "Jogadores"),
|
||||
if (players.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Text("Nenhum jogador adicionado.", style: TextStyle(color: Colors.grey)),
|
||||
)
|
||||
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),
|
||||
],
|
||||
),
|
||||
@@ -60,100 +93,14 @@ class TeamStatsPage extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _controller.showAddPersonDialog(context, team.id),
|
||||
heroTag: 'player_fab',
|
||||
onPressed: () => _controller.showAddPersonDialog(context, widget.team.id),
|
||||
backgroundColor: const Color(0xFF00C853),
|
||||
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) {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -164,7 +111,7 @@ class TeamStatsPage extends StatelessWidget {
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("Cancelar")),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_controller.deletePerson(team.id, person.id);
|
||||
_controller.deletePerson(widget.team.id, person.id);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
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) ---
|
||||
|
||||
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) {
|
||||
_emitMembers(teamId);
|
||||
// Pequeno delay para garantir que o Stream ouve a primeira emissão
|
||||
Future.delayed(Duration.zero, () => _emitMembers(teamId));
|
||||
return _streamController.stream;
|
||||
}
|
||||
|
||||
// Função auxiliar para atualizar quem está a ouvir
|
||||
void _emitMembers(String teamId) {
|
||||
final list = TeamController.members
|
||||
final filtered = _mockMembers
|
||||
.where((m) => m['team_id'] == teamId)
|
||||
.map((json) => Person.fromMap(json))
|
||||
.map((m) => Person.fromMap(m))
|
||||
.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) {
|
||||
TeamController.members.removeWhere((m) => m['id'] == personId);
|
||||
_emitMembers(teamId);
|
||||
_mockMembers.removeWhere((m) => m['id'] == personId);
|
||||
_emitMembers(teamId); // Atualiza o ecrã
|
||||
}
|
||||
|
||||
// 3. DIÁLOGOS
|
||||
void showAddPersonDialog(BuildContext context, String teamId) {
|
||||
_showForm(context, teamId: teamId);
|
||||
}
|
||||
@@ -220,7 +173,7 @@ class StatsController {
|
||||
final isEdit = person != null;
|
||||
final nameCtrl = TextEditingController(text: person?.name ?? '');
|
||||
final numCtrl = TextEditingController(text: person?.number ?? '');
|
||||
String type = person?.type ?? 'Jogador';
|
||||
String selectedType = person?.type ?? 'Jogador';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -230,41 +183,80 @@ class StatsController {
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(controller: nameCtrl, decoration: const InputDecoration(labelText: "Nome")),
|
||||
DropdownButton<String>(
|
||||
value: type,
|
||||
isExpanded: true,
|
||||
items: ["Jogador", "Treinador"].map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(),
|
||||
onChanged: (v) => setState(() => type = v!),
|
||||
TextField(
|
||||
controller: nameCtrl,
|
||||
decoration: const InputDecoration(labelText: "Nome"),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
),
|
||||
if (type == "Jogador")
|
||||
TextField(controller: numCtrl, decoration: const InputDecoration(labelText: "Número"), keyboardType: TextInputType.number),
|
||||
const SizedBox(height: 10),
|
||||
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: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("Sair")),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text("Cancelar")
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF00C853)),
|
||||
onPressed: () {
|
||||
if (nameCtrl.text.trim().isEmpty) return;
|
||||
|
||||
String? numeroFinal = (selectedType == "Treinador")
|
||||
? null
|
||||
: (numCtrl.text.trim().isEmpty ? null : numCtrl.text.trim());
|
||||
|
||||
if (isEdit) {
|
||||
final idx = TeamController.members.indexWhere((m) => m['id'] == person.id);
|
||||
TeamController.members[idx] = {'id': person.id, 'team_id': teamId, 'name': nameCtrl.text, 'type': type, 'number': numCtrl.text};
|
||||
// ATUALIZAR NA LISTA LOCAL
|
||||
final index = _mockMembers.indexWhere((m) => m['id'] == person!.id);
|
||||
if (index != -1) {
|
||||
_mockMembers[index] = {
|
||||
'id': person!.id,
|
||||
'team_id': teamId,
|
||||
'name': nameCtrl.text.trim(),
|
||||
'type': selectedType,
|
||||
'number': numeroFinal,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
TeamController.members.add({
|
||||
'id': DateTime.now().toString(),
|
||||
// CRIAR NA LISTA LOCAL (Gera ID com a data atual)
|
||||
_mockMembers.add({
|
||||
'id': DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
'team_id': teamId,
|
||||
'name': nameCtrl.text,
|
||||
'type': type,
|
||||
'number': numCtrl.text
|
||||
'name': nameCtrl.text.trim(),
|
||||
'type': selectedType,
|
||||
'number': numeroFinal,
|
||||
});
|
||||
}
|
||||
|
||||
// Atualiza a UI
|
||||
_emitMembers(teamId);
|
||||
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:playmaker/screens/team_stats_page.dart';
|
||||
import '../models/team_model.dart';
|
||||
import '../controllers/team_controller.dart';
|
||||
import '../screens/team_stats_page.dart';
|
||||
|
||||
class TeamCard extends StatelessWidget {
|
||||
final Team team;
|
||||
final TeamController controller;
|
||||
@@ -17,40 +18,37 @@ class TeamCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
color: Colors.white,
|
||||
elevation: 3,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
|
||||
leading: Stack(
|
||||
clipBehavior: Clip.none, // Permite que a estrela flutue ligeiramente fora do círculo
|
||||
// --- 1. IMAGEM + FAVORITO ---
|
||||
leading: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// 1. IMAGEM DA EQUIPA
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
|
||||
? NetworkImage(team.imageUrl)
|
||||
backgroundImage: (team.imageUrl.isNotEmpty && team.imageUrl.startsWith('http'))
|
||||
? NetworkImage(team.imageUrl)
|
||||
: null,
|
||||
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
|
||||
child: (team.imageUrl.isEmpty || !team.imageUrl.startsWith('http'))
|
||||
? Text(
|
||||
team.imageUrl.isEmpty ? "🏀" : team.imageUrl,
|
||||
style: const TextStyle(fontSize: 24),
|
||||
)
|
||||
)
|
||||
: null,
|
||||
),
|
||||
|
||||
// 2. BOTÃO DA ESTRELA (Favorito)
|
||||
Positioned(
|
||||
left: -15, // Posiciona à esquerda da imagem
|
||||
left: -15,
|
||||
top: -10,
|
||||
child: IconButton(
|
||||
// O segredo está em colocar o shadow dentro do Icon:
|
||||
icon: Icon(
|
||||
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,
|
||||
shadows: [
|
||||
Shadow(
|
||||
@@ -65,13 +63,13 @@ class TeamCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
|
||||
// --- NOME DA EQUIPA ---
|
||||
// --- 2. TÍTULO ---
|
||||
title: Text(
|
||||
team.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
|
||||
// --- SUBTÍTULO (CONTAGEM E TEMPORADA) ---
|
||||
// --- 3. SUBTÍTULO (Contagem + Época) ---
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
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(
|
||||
width: 80,
|
||||
width: 96, // Aumentei um pouco para caberem bem os dois botões
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@@ -112,6 +110,7 @@ class TeamCard extends StatelessWidget {
|
||||
tooltip: 'Ver Estatísticas',
|
||||
icon: const Icon(Icons.bar_chart_rounded, color: Colors.blue),
|
||||
onPressed: () {
|
||||
// CORRIGIDO: Agora chama a classe TeamStatsPage corretamente
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -131,6 +130,8 @@ class TeamCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Função de confirmação de exclusão
|
||||
void _confirmDelete(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -139,22 +140,23 @@ class TeamCard extends StatelessWidget {
|
||||
content: Text('Tens a certeza que queres eliminar "${team.name}"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancelar')
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
controller.deleteTeam(team.id);
|
||||
controller.deleteTeam(team.id);
|
||||
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 {
|
||||
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')),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE74C3C)),
|
||||
onPressed: () {
|
||||
if (_nameController.text.trim().isNotEmpty) {
|
||||
widget.onConfirm(
|
||||
_nameController.text.trim(),
|
||||
_selectedSeason,
|
||||
_imageController.text.trim()
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
onPressed: () {
|
||||
if (_nameController.text.trim().isNotEmpty) {
|
||||
widget.onConfirm(
|
||||
_nameController.text.trim(),
|
||||
_selectedSeason,
|
||||
_imageController.text.trim(),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
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"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.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