diff --git a/app/src/main/java/com/example/vdcscore/ui/cup/CupPhase.java b/app/src/main/java/com/example/vdcscore/ui/cup/CupPhase.java new file mode 100644 index 0000000..cfca8ff --- /dev/null +++ b/app/src/main/java/com/example/vdcscore/ui/cup/CupPhase.java @@ -0,0 +1,36 @@ +package com.example.vdcscore.ui.cup; + +import java.util.List; + +public class CupPhase { + private String name; + private List matches; + + public CupPhase() { + } + + public CupPhase(String name) { + this.name = name; + } + + public CupPhase(String name, List matches) { + this.name = name; + this.matches = matches; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getMatches() { + return matches; + } + + public void setMatches(List matches) { + this.matches = matches; + } +} diff --git a/app/src/main/java/com/example/vdcscore/ui/cup/CupPhasesAdapter.java b/app/src/main/java/com/example/vdcscore/ui/cup/CupPhasesAdapter.java new file mode 100644 index 0000000..be0f86a --- /dev/null +++ b/app/src/main/java/com/example/vdcscore/ui/cup/CupPhasesAdapter.java @@ -0,0 +1,57 @@ +package com.example.vdcscore.ui.cup; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.vdcscore.databinding.ItemCupPhaseBinding; +import com.example.vdcscore.ui.gallery.MatchesAdapter; + +import java.util.ArrayList; +import java.util.List; + +public class CupPhasesAdapter extends RecyclerView.Adapter { + + private List phasesList = new ArrayList<>(); + + public void setPhases(List phases) { + this.phasesList = phases; + notifyDataSetChanged(); + } + + @NonNull + @Override + public PhaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemCupPhaseBinding binding = ItemCupPhaseBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false); + return new PhaseViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull PhaseViewHolder holder, int position) { + CupPhase phase = phasesList.get(position); + holder.binding.textPhaseName.setText(phase.getName()); + + MatchesAdapter matchesAdapter = new MatchesAdapter(); + matchesAdapter.setMatches(phase.getMatches() != null ? phase.getMatches() : new ArrayList<>()); + holder.binding.recyclerPhaseMatches.setAdapter(matchesAdapter); + holder.binding.recyclerPhaseMatches.setLayoutManager( + new androidx.recyclerview.widget.LinearLayoutManager(holder.binding.getRoot().getContext())); + } + + @Override + public int getItemCount() { + return phasesList.size(); + } + + static class PhaseViewHolder extends RecyclerView.ViewHolder { + ItemCupPhaseBinding binding; + + PhaseViewHolder(ItemCupPhaseBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } +} diff --git a/app/src/main/java/com/example/vdcscore/utils/FirebaseErrorUtils.java b/app/src/main/java/com/example/vdcscore/utils/FirebaseErrorUtils.java new file mode 100644 index 0000000..9ce2e97 --- /dev/null +++ b/app/src/main/java/com/example/vdcscore/utils/FirebaseErrorUtils.java @@ -0,0 +1,76 @@ +package com.example.vdcscore.utils; + +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException; +import com.google.firebase.auth.FirebaseAuthInvalidUserException; +import com.google.firebase.auth.FirebaseAuthUserCollisionException; +import com.google.firebase.auth.FirebaseAuthWeakPasswordException; +import com.google.firebase.FirebaseNetworkException; + +public class FirebaseErrorUtils { + + public static String getErrorMessagePt(Exception exception) { + if (exception == null) { + return "Ocorreu um erro desconhecido."; + } + + if (exception instanceof FirebaseAuthInvalidCredentialsException) { + return "O e-mail ou a palavra-passe estão incorretos."; + } + + if (exception instanceof FirebaseAuthInvalidUserException) { + String errorCode = ((FirebaseAuthInvalidUserException) exception).getErrorCode(); + if ("ERROR_USER_DISABLED".equals(errorCode)) { + return "Esta conta de utilizador foi desativada."; + } + return "Não existe nenhuma conta registada com este e-mail."; + } + + if (exception instanceof FirebaseAuthUserCollisionException) { + return "Já existe uma conta registada com este e-mail."; + } + + if (exception instanceof FirebaseAuthWeakPasswordException) { + return "A palavra-passe introduzida é demasiado fraca. Introduza pelo menos 6 caracteres."; + } + + if (exception instanceof FirebaseNetworkException) { + return "Sem ligação à internet. Por favor, verifique a sua rede."; + } + + // Fallbacks baseados na mensagem da exceção + String message = exception.getMessage(); + if (message != null) { + String lowerMessage = message.toLowerCase(); + if (lowerMessage.contains("badly formatted") || lowerMessage.contains("invalid email")) { + return "O formato do e-mail introduzido é inválido."; + } + if (lowerMessage.contains("no user record") || lowerMessage.contains("user not found") || lowerMessage.contains("user-not-found")) { + return "Não existe nenhuma conta registada com este e-mail."; + } + if (lowerMessage.contains("wrong password") || lowerMessage.contains("invalid password") || lowerMessage.contains("wrong-password")) { + return "O e-mail ou a palavra-passe estão incorretos."; + } + if (lowerMessage.contains("email already in use") || lowerMessage.contains("already exists") || lowerMessage.contains("email-already-in-use")) { + return "Já existe uma conta registada com este e-mail."; + } + if (lowerMessage.contains("network") || lowerMessage.contains("connection")) { + return "Sem ligação à internet. Por favor, verifique a sua rede."; + } + if (lowerMessage.contains("recent login required")) { + return "Por segurança, precisa de terminar sessão e voltar a entrar para realizar esta alteração."; + } + } + + return "Erro: " + (message != null ? message : "desconhecido."); + } + + public static String sanitizeKey(String key) { + if (key == null) return null; + return key.replace(".", "_") + .replace("#", "_") + .replace("$", "_") + .replace("[", "_") + .replace("]", "_") + .replace("/", "_"); + } +} diff --git a/app/src/main/res/drawable/ic_arrow_forward.xml b/app/src/main/res/drawable/ic_arrow_forward.xml new file mode 100644 index 0000000..9b84399 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_forward.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_star.xml b/app/src/main/res/drawable/ic_star.xml new file mode 100644 index 0000000..8ea1897 --- /dev/null +++ b/app/src/main/res/drawable/ic_star.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_border.xml b/app/src/main/res/drawable/ic_star_border.xml new file mode 100644 index 0000000..7073c66 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_border.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_criarconta.xml b/app/src/main/res/layout/activity_criarconta.xml index 8dd1d4d..6de7294 100644 --- a/app/src/main/res/layout/activity_criarconta.xml +++ b/app/src/main/res/layout/activity_criarconta.xml @@ -105,13 +105,13 @@ + android:fontFamily="@font/font_ibm_plex_sans" + android:text="Palavra-passe" + android:textColor="@color/text_1" + android:textSize="13sp" + android:textStyle="bold" /> + android:fontFamily="@font/font_ibm_plex_sans" + android:text="Confirmar Palavra-passe" + android:textColor="@color/text_1" + android:textSize="13sp" + android:textStyle="bold" /> + android:focusable="true" + android:fontFamily="@font/font_ibm_plex_sans" + android:text="Clique aqui para iniciar sessão" + android:textColor="@color/brand" + android:textSize="13sp" + android:textStyle="bold" /> diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 08b2c50..d3584c3 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -60,16 +60,16 @@ android:paddingTop="20dp" android:paddingBottom="20dp"> - + - + + android:focusable="true" + android:fontFamily="@font/font_ibm_plex_sans" + android:text="Esqueceu-se da palavra-passe?" + android:textColor="@color/text_2" + android:textSize="12sp" + android:textStyle="bold" /> diff --git a/app/src/main/res/layout/item_cup_phase.xml b/app/src/main/res/layout/item_cup_phase.xml new file mode 100644 index 0000000..1735171 --- /dev/null +++ b/app/src/main/res/layout/item_cup_phase.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + diff --git a/memories/repo/vdcscore-overview.md b/memories/repo/vdcscore-overview.md new file mode 100644 index 0000000..5f9e8c8 --- /dev/null +++ b/memories/repo/vdcscore-overview.md @@ -0,0 +1,63 @@ +# VdcScore - Visão Geral do Projeto + +## O que é +App Android (Java) chamada **VdcScore** que exibe dados de campeonatos locais de futebol (AFAVCD) em tempo real. + +## Arquitetura +- **Scraper Java** (projeto separado `scrapper/`): Aplicaçãp autónoma que faz scraping da API HTML/JSON da AFAVCD, processa os dados e escreve no Firebase Realtime Database. +- **Firebase Realtime Database**: Centraliza todos os dados (Single Source of Truth). +- **App Android VdcScore**: Cliente de leitura que consome os dados do Firebase em tempo real via ValueEventListener. + +## Estrutura de Dados no Firebase +``` +Senior/ + standings/ - Tabelas classificativas por clube + journeys/ - Jornadas com jogos (homeTeam, awayTeam, scores, date, field, matchReportUrl) + players/ - Plantéis de cada equipa + melhores_marcadores/ - Top scorers (Seniores e Juniores) + noticias/ - Notícias da AFAVCD + live_matches/ - Jogos preparados para acompanhamento em direto +Users/ + UID/ - Utilizadores (email, favoriteClub) +``` + +## Componentes Principais da App Android +- **models/**: `Club`, `Game`/`Match`, `Jornada`, `Player`, `TopScorer`, `News` +- **ui/**: Fragments + ViewModels para cada secção + - `home/` - Classificações + - `gallery/` - Jornadas/Jogos (MatchesAdapter, Match.java) + - `livegames/` - Jogos em direto + - `clubs/` - Equipas/Plantéis + - `top_scorers/` - Melhores Marcadores + - `news/` - Notícias (ecrã principal por defeito) + - `definicoes/` - Definições +- **Autenticação**: LoginActivity, CriarContaActivity, RecuperarPasswordActivity, MainActivity + +## Tecnologias +- **Scraper**: JSoup, GSON, Firebase Admin SDK, Gradle +- **Android**: ViewBinding, Glide, Firebase Auth, Firebase Realtime Database, Navigation Component, RecyclerView + +## Estado Atual +- Scraper de Standings/Jornadas: ✅ Funcional +- Scraper de Melhores Marcadores: ✅ Funcional +- Scraper de Notícias: ✅ Funcional +- Scraper de Plantéis (PlayersScraper): 🔄 Em desenvolvimento +- UI Jornadas: ✅ Cartões premium com Glide, Ficha de Jogo +- UI Melhores Marcadores: ✅ Ecrã completo +- UI Notícias: ✅ No ecrã principal (Ínicio) +- UI Classificações: ✅ Funcional +- Autenticação Firebase: ✅ Implementada +- Live Matches: ✅ Preparação de jogos futuros + +## Tarefas Pendentes +- Completar PlayersScraper (plantéis completos) +- Sistema Offline (Firebase cache local) +- Push Notifications (FCM) +- Testes finais de UI para campos opcionais null + +## Convenções Importantes +- Models Android devem bater certo com models Scraper (nomes de atributos) +- Valores numéricos vêm como String da API - fazer parse para Integer +- Campos opcionais podem vir vazios (matchReportUrl, data) - UI deve lidar com null +- Chaves Firebase em minúsculas (standings, journeys, players) +- Serviços: `service-account.json` nunca no version control