app atualizada para apresentação

This commit is contained in:
2026-06-16 14:23:41 +01:00
parent ae1bc0b3df
commit b1ba711a37
10 changed files with 348 additions and 44 deletions

View File

@@ -0,0 +1,36 @@
package com.example.vdcscore.ui.cup;
import java.util.List;
public class CupPhase {
private String name;
private List<com.example.vdcscore.ui.gallery.Match> matches;
public CupPhase() {
}
public CupPhase(String name) {
this.name = name;
}
public CupPhase(String name, List<com.example.vdcscore.ui.gallery.Match> matches) {
this.name = name;
this.matches = matches;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<com.example.vdcscore.ui.gallery.Match> getMatches() {
return matches;
}
public void setMatches(List<com.example.vdcscore.ui.gallery.Match> matches) {
this.matches = matches;
}
}

View File

@@ -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<CupPhasesAdapter.PhaseViewHolder> {
private List<CupPhase> phasesList = new ArrayList<>();
public void setPhases(List<CupPhase> 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;
}
}
}

View File

@@ -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("/", "_");
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24M12,15.4L8.24,17.67L9.24,13.38L5.92,10.5L10.3,10.13L12,6.1L13.7,10.13L18.08,10.5L14.76,13.38L15.76,17.67L12,15.4Z"/>
</vector>

View File

@@ -105,13 +105,13 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/font_ibm_plex_sans" android:layout_marginStart="4dp"
android:text="Password"
android:textColor="@color/text_1"
android:textStyle="bold"
android:textSize="13sp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:layout_marginStart="4dp"/> android:fontFamily="@font/font_ibm_plex_sans"
android:text="Palavra-passe"
android:textColor="@color/text_1"
android:textSize="13sp"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/editPassword2" android:id="@+id/editPassword2"
@@ -131,13 +131,13 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/font_ibm_plex_sans" android:layout_marginStart="4dp"
android:text="Confirmar Password"
android:textColor="@color/text_1"
android:textStyle="bold"
android:textSize="13sp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:layout_marginStart="4dp"/> android:fontFamily="@font/font_ibm_plex_sans"
android:text="Confirmar Palavra-passe"
android:textColor="@color/text_1"
android:textSize="13sp"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/editConfirmPassword" android:id="@+id/editConfirmPassword"
@@ -187,13 +187,13 @@
android:id="@+id/txtGoLogin" android:id="@+id/txtGoLogin"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/font_ibm_plex_sans"
android:text="Sign In"
android:textColor="@color/brand"
android:textStyle="bold"
android:textSize="13sp"
android:clickable="true" android:clickable="true"
android:focusable="true"/> 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" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -63,13 +63,13 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/font_bebas_neue"
android:text="LOGIN"
android:textSize="32sp"
android:textColor="@color/text_1"
android:letterSpacing="0.04"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginBottom="32dp" /> android:layout_marginBottom="32dp"
android:fontFamily="@font/font_bebas_neue"
android:letterSpacing="0.04"
android:text="Iniciar Sessão"
android:textColor="@color/text_1"
android:textSize="32sp" />
<!-- Email Label --> <!-- Email Label -->
<TextView <TextView
@@ -102,13 +102,13 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/font_ibm_plex_sans" android:layout_marginStart="4dp"
android:text="Password"
android:textColor="@color/text_1"
android:textStyle="bold"
android:textSize="13sp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:layout_marginStart="4dp"/> android:fontFamily="@font/font_ibm_plex_sans"
android:text="Palavra-passe"
android:textColor="@color/text_1"
android:textSize="13sp"
android:textStyle="bold" />
<!-- Password Input --> <!-- Password Input -->
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
@@ -152,13 +152,13 @@
android:id="@+id/txtForgotPassword" android:id="@+id/txtForgotPassword"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/font_ibm_plex_sans"
android:text="Esqueceu a senha?"
android:textColor="@color/text_2"
android:textStyle="bold"
android:textSize="12sp"
android:clickable="true" android:clickable="true"
android:focusable="true"/> 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" />
</LinearLayout> </LinearLayout>
<!-- Login Button --> <!-- Login Button -->

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/bg_surface">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Phase Header -->
<TextView
android:id="@+id/text_phase_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:fontFamily="@font/font_bebas_neue"
android:textSize="20sp"
android:letterSpacing="0.04"
android:textColor="@color/text_1"
android:text="1ª Eliminatoria 1º Mão" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/border" />
<!-- Matches Container -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_phase_matches"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:padding="8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -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