melhores marcadores adicionados

This commit is contained in:
2026-04-24 16:44:50 +01:00
parent ce16ff59b6
commit 8784cc4975
12 changed files with 789 additions and 0 deletions

25
README.md Normal file
View File

@@ -0,0 +1,25 @@
# VdcScore (Campeonato Inter Freguesias de Vila do Conde)
Bem-vindo ao projeto **VdcScore**. Este sistema é composto por duas componentes principais projetadas para extrair, processar e apresentar dados do Campeonato Inter Freguesias de Vila do Conde (AFAVCD).
## Objetivo do Projeto
Fornecer uma aplicação móvel interativa e moderna aos utilizadores finais, permitindo-lhes visualizar toda a informação sobre clubes, jogos, classificações e jogadores em tempo real. A App Android não possui uma backend tradicional (API REST), mas sim uma arquitetura orientada a eventos usando **Firebase Realtime Database**. Os dados no Firebase são mantidos sempre atualizados por um **Scraper Java**, que corre autonomamente para ler dados oficiais do website/API da associação de futebol.
## Componentes do Sistema
1. **VdcScore App (Android):** Uma aplicação nativa Android (`VdcScore_Project/VdcScore`) desenhada para consumidores finais. Inclui autenticação (Firebase Auth) e lê dados diretamente do Firebase Database.
2. **Scraper (Java):** Uma aplicação Java isolada (`VdcScore_Project/scrapper`) que funciona como um worker. Extrai (scrapes) dados e envia as atualizações para o Firebase.
## Como Navegar na Documentação
Para suportar o desenvolvimento contínuo (seja por humanos ou por Agentes de Inteligência Artificial), criámos um conjunto de documentos detalhados na pasta `docs/`. Recomendamos ler pela seguinte ordem:
- [01 - Arquitetura (Fluxo de Dados)](docs/01_ARQUITETURA.md)
- [02 - Projeto Scraper (Extração de Dados)](docs/02_PROJETO_SCRAPER.md)
- [03 - Projeto Android (App & UI)](docs/03_PROJETO_ANDROID.md)
- [04 - Schema da Base de Dados (Firebase)](docs/04_SCHEMA_BASE_DADOS.md)
- [05 - Progresso e Estado Atual](docs/05_PROGRESSO_E_ESTADO_ATUAL.md)
## Dicas para IA / LLMs
- **Contexto**: Quando assumires este projeto, verifica primeiro o ficheiro `05_PROGRESSO_E_ESTADO_ATUAL.md` para entender onde a equipa parou e quais são os próximos passos.
- **Base de Dados**: O schema não deve ser alterado à toa num dos projetos sem alinhar com o outro. Ambos partilham o mesmo design estrutural do Firebase. Ver `04_SCHEMA_BASE_DADOS.md`.

View File

@@ -0,0 +1,78 @@
package com.example.vdcscore.models;
import com.google.firebase.database.PropertyName;
import java.io.Serializable;
public class TopScorer implements Serializable {
private String playerName;
private String playerPhoto;
private String clubName;
private String clubLogo;
private int goals;
private int position;
public TopScorer() {
// Required for Firebase
}
@PropertyName("playerName")
public String getPlayerName() {
return playerName;
}
@PropertyName("playerName")
public void setPlayerName(String playerName) {
this.playerName = playerName;
}
@PropertyName("playerPhoto")
public String getPlayerPhoto() {
return playerPhoto;
}
@PropertyName("playerPhoto")
public void setPlayerPhoto(String playerPhoto) {
this.playerPhoto = playerPhoto;
}
@PropertyName("clubName")
public String getClubName() {
return clubName;
}
@PropertyName("clubName")
public void setClubName(String clubName) {
this.clubName = clubName;
}
@PropertyName("clubLogo")
public String getClubLogo() {
return clubLogo;
}
@PropertyName("clubLogo")
public void setClubLogo(String clubLogo) {
this.clubLogo = clubLogo;
}
@PropertyName("goals")
public int getGoals() {
return goals;
}
@PropertyName("goals")
public void setGoals(int goals) {
this.goals = goals;
}
@PropertyName("position")
public int getPosition() {
return position;
}
@PropertyName("position")
public void setPosition(int position) {
this.position = position;
}
}

View File

@@ -0,0 +1,89 @@
package com.example.vdcscore.ui.topscorers;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.example.vdcscore.R;
import com.example.vdcscore.models.TopScorer;
import java.util.ArrayList;
import java.util.List;
public class TopScorersAdapter extends RecyclerView.Adapter<TopScorersAdapter.ViewHolder> {
private List<TopScorer> scorers = new ArrayList<>();
public void setTopScorers(List<TopScorer> scorers) {
this.scorers = (scorers != null) ? scorers : new ArrayList<>();
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_top_scorer, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
TopScorer scorer = scorers.get(position);
holder.textPosition.setText(scorer.getPosition() + "º");
holder.textPlayerName.setText(isValid(scorer.getPlayerName()) ? scorer.getPlayerName() : "Jogador");
holder.textClubName.setText(isValid(scorer.getClubName()) ? scorer.getClubName() : "Clube");
holder.textGoals.setText(String.valueOf(scorer.getGoals()));
// Carregar Foto do Jogador
Glide.with(holder.itemView.getContext())
.load(scorer.getPlayerPhoto())
.placeholder(R.drawable.ic_menu_camera)
.error(R.drawable.ic_menu_camera)
.circleCrop()
.into(holder.imgPlayerPhoto);
// Carregar Logótipo do Clube
Glide.with(holder.itemView.getContext())
.load(scorer.getClubLogo())
.placeholder(R.drawable.ic_menu_gallery)
.error(R.drawable.ic_menu_gallery)
.circleCrop()
.into(holder.imgClubLogo);
}
private boolean isValid(String text) {
return text != null && !text.trim().isEmpty() && !text.equalsIgnoreCase("null");
}
@Override
public int getItemCount() {
return scorers.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView textPosition;
public final TextView textPlayerName;
public final TextView textClubName;
public final TextView textGoals;
public final ImageView imgPlayerPhoto;
public final ImageView imgClubLogo;
public ViewHolder(View view) {
super(view);
textPosition = view.findViewById(R.id.text_position);
textPlayerName = view.findViewById(R.id.text_player_name);
textClubName = view.findViewById(R.id.text_club_name);
textGoals = view.findViewById(R.id.text_goals);
imgPlayerPhoto = view.findViewById(R.id.img_player_photo);
imgClubLogo = view.findViewById(R.id.img_club_logo);
}
}
}

View File

@@ -0,0 +1,130 @@
package com.example.vdcscore.ui.topscorers;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.vdcscore.R;
import com.example.vdcscore.models.TopScorer;
import com.google.android.material.button.MaterialButtonToggleGroup;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class TopScorersFragment extends Fragment {
private RecyclerView recyclerTopScorers;
private TopScorersAdapter adapter;
private MaterialButtonToggleGroup toggleGroupCategory;
private TextView textEmptyState;
private DatabaseReference mDatabase;
private String currentCategory = "seniores";
private ValueEventListener currentListener;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_top_scorers, container, false);
recyclerTopScorers = root.findViewById(R.id.recycler_top_scorers);
toggleGroupCategory = root.findViewById(R.id.toggleGroupCategory);
textEmptyState = root.findViewById(R.id.text_empty_state);
adapter = new TopScorersAdapter();
recyclerTopScorers.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerTopScorers.setAdapter(adapter);
toggleGroupCategory.addOnButtonCheckedListener((group, checkedId, isChecked) -> {
if (isChecked) {
if (checkedId == R.id.btn_seniores) {
currentCategory = "seniores";
} else if (checkedId == R.id.btn_juniores) {
currentCategory = "juniores";
}
fetchTopScorers();
}
});
fetchTopScorers();
return root;
}
private void fetchTopScorers() {
if (mDatabase != null && currentListener != null) {
mDatabase.removeEventListener(currentListener);
}
textEmptyState.setVisibility(View.VISIBLE);
textEmptyState.setText("A carregar dados...");
recyclerTopScorers.setVisibility(View.GONE);
mDatabase = FirebaseDatabase.getInstance().getReference("marcadores").child(currentCategory);
currentListener = new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
if (getContext() == null) return;
List<TopScorer> scorersList = new ArrayList<>();
for (DataSnapshot data : snapshot.getChildren()) {
TopScorer scorer = data.getValue(TopScorer.class);
if (scorer != null) {
scorersList.add(scorer);
}
}
// Ordenar por golos descrescente
Collections.sort(scorersList, new Comparator<TopScorer>() {
@Override
public int compare(TopScorer s1, TopScorer s2) {
return Integer.compare(s2.getGoals(), s1.getGoals());
}
});
if (scorersList.isEmpty()) {
textEmptyState.setText("Ainda não existem marcadores registados.");
textEmptyState.setVisibility(View.VISIBLE);
recyclerTopScorers.setVisibility(View.GONE);
} else {
textEmptyState.setVisibility(View.GONE);
recyclerTopScorers.setVisibility(View.VISIBLE);
adapter.setTopScorers(scorersList);
}
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
if (getContext() != null) {
Toast.makeText(getContext(), "Erro ao carregar: " + error.getMessage(), Toast.LENGTH_SHORT).show();
}
}
};
mDatabase.addValueEventListener(currentListener);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (mDatabase != null && currentListener != null) {
mDatabase.removeEventListener(currentListener);
}
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#F0F2F5"/>
</shape>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F7FA"
tools:context=".ui.topscorers.TopScorersFragment">
<!-- Title and Category Toggle -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="4dp"
app:cardCornerRadius="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Melhores Marcadores"
android:textSize="20sp"
android:textColor="#1A237E"
android:textStyle="bold"
android:layout_marginBottom="12dp" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/toggleGroupCategory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleSelection="true"
app:checkedButton="@+id/btn_seniores">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_seniores"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Seniores" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_juniores"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Juniores" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Empty State -->
<TextView
android:id="@+id/text_empty_state"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="A carregar dados..."
android:textAlignment="center"
android:gravity="center"
android:textSize="16sp"
android:visibility="gone" />
<!-- RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_top_scorers"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:paddingBottom="16dp"
android:clipToPadding="false"
tools:listitem="@layout/item_top_scorer" />
</LinearLayout>

View File

@@ -0,0 +1,115 @@
<?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_marginVertical="6dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<!-- Position Badge -->
<TextView
android:id="@+id/text_position"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/bg_circle_light_gray"
android:text="1"
android:textColor="#333333"
android:textSize="14sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginEnd="12dp" />
<!-- Player Photo -->
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/img_player_photo"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_menu_gallery"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.CornerSize50Percent"
android:layout_marginEnd="12dp" />
<!-- Player & Club Details -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/text_player_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nome do Jogador"
android:textColor="#1A237E"
android:textSize="16sp"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="4dp">
<ImageView
android:id="@+id/img_club_logo"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_menu_gallery"
android:layout_marginEnd="6dp" />
<TextView
android:id="@+id/text_club_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nome do Clube"
android:textColor="#757575"
android:textSize="13sp"
android:maxLines="1"
android:ellipsize="end" />
</LinearLayout>
</LinearLayout>
<!-- Goals Counter -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingStart="12dp">
<TextView
android:id="@+id/text_goals"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="15"
android:textColor="#FF6D00"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Golos"
android:textColor="#9E9E9E"
android:textSize="11sp"
android:textAllCaps="true" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

32
docs/01_ARQUITETURA.md Normal file
View File

@@ -0,0 +1,32 @@
# 01 - Arquitetura de Software
A arquitetura do VdcScore foi desenhada para resolver o problema de que as origens dos dados de campeonatos locais raramente oferecem uma API oficial, em tempo real e de uso público que possa ser consumida diretamente por dezenas de aplicações clientes sem problemas de estabilidade e segurança.
## Diagrama de Fluxo (Data Flow)
```mermaid
graph TD
A[AFAVCD Web API / JSON] -->|1. Request/Scrape| B(Scraper Java)
B -->|2. Limpeza e Parsing (GSON/Jsoup)| C{Processamento}
C -->|3. Firebase Admin SDK| D[(Firebase Realtime Database)]
D -->|4. Sync Em Tempo Real| E(App Android VdcScore)
E -->|5. Exibe Dados| F[Utilizador Final]
```
## Como a Informação flui
### 1. Fonte de Dados
O sistema baseia-se nos dados provenientes do website oficial da associação (AFAVCD). Eles são obtidos essencialmente através de endpoints de "API" ocultos ou HTML bruto.
### 2. O Scraper
O **Scraper** é uma aplicação Java isolada. Esta aplicação tem de ser agendada (ex: cronjob num servidor) para correr periodicamente.
- Recolhe dados como listas de jogos (`/jorneys`), resultados, estatísticas dos clubes e plantéis (jogadores).
- Compara a nova informação, converte-a para a nossa estrutura de classes, e utiliza a biblioteca `firebase-admin` (via JSON account de serviço) para fazer update à **Firebase Realtime Database**.
- Ao usar este "middle-man" garantimos que a App Android não tem de saber lidar com a complexidade e lentidão de extrair os dados na hora (nem sobrecarregar a fonte oficial com requests de todos os utilizadores).
### 3. A Centralização (Firebase Realtime Database)
A base de dados funciona como a única fonte da verdade (*Single Source of Truth*). Os nós de dados são altamente descritivos e simples. Sempre que o Scraper faz push de dados novos, os utilizadores conectados recebem as alterações graças ao modelo de websockets da Realtime DB.
### 4. A App Cliente
A App Android está ligada diretamente à Firebase com permissões de **Leitura Apenas** (Read-Only) para a maioria das coleções (exceto para gestão de utilizadores e favoritos, caso existam).
O emparelhamento é feito via Data Models (ex: `Game.java`, `Club.java`) que correspondem *exatamente* ao formato JSON injetado pelo Scraper.

View File

@@ -0,0 +1,30 @@
# 02 - Projeto Scraper Java
A pasta irmão `scrapper` contém uma aplicação autónoma em puro Java com a responsabilidade de raspar (scrapping) a informação dos clubes, jogos e tabelas.
## Ficheiros Críticos
O projeto está implementado em `src/main/java/org/example/`. Destacam-se:
- `Main.java`: Onde o projeto arranca. Configura a ligação com Firebase, cria a Realtime DB reference e também executa as ações principais (ex. extrair info de clubes).
- `StandingsScraper.java`: Classe responsável pela extração das Tabelas Classificativas (Standings) e lista de jogos (Jornadas) baseando-se num endpoint JSON fornecido pela AFAVCD.
- `PlayersScraper.java`: Esqueleto preparado para iterar pela lista de plantéis das equipas.
- Diretório `models/`: Classes de modelos Java que refletem a estrutura exata do que será enviado ao Firebase.
## Tecnologias Usadas
1. **JSoup**: Utilizado para enviar HTTP requests a URLs tradicionais e efetuar o parsing de documentos HTML usando CSS Selectors (quando a informação não é fornecida via JSON).
2. **GSON (Google JSON)**: Utilizado quando a associação fornece endpoints JSON. O Gson pega nas "Strings" JSON devolvidas pela API e converte para objetos (models) Java, facilitando o acesso às propriedades.
3. **Firebase Admin SDK**: Ao contrário do Android (que usa Auth regular de cliente), o Scraper tem privilégios de Admin. Utiliza a "chave de serviço" de Firebase (vulgarmente no ficheiro `service-account.json` que deve manter-se sempre fora do controlo de versões, via `.gitignore`).
4. **Gradle**: O ciclo de build e os pacotes são geridos pelo Gradle (usando sintaxe `build.gradle.kts`).
## O Fluxo Padrão do Scraper
1. A função `main` carrega o Firebase Options, passando as credenciais para obter acesso total à base de dados de Firebase.
2. Invocam-se os Scrapers. Por exemplo, `StandingsScraper.scrapeAndSync()`.
3. O Scraper descarrega o payload (JSON ou HTML).
4. O Scraper itera os elementos (Equipas, Jogos). Cria uma instância da classe Model relevante (ex: `TeamStanding.java`).
5. A referência Firebase é chamada (e.g. `ref.child("Senior").child("standings")`) e o objeto é passado por `setValueAsync()`.
## Notas de Evolução
- *Limites de Rate*: Quando o projeto aumentar a sua abrangência (extrair todos os jogadores para todas as equipas), deve-se implementar delays (`Thread.sleep()`) para prevenir que o servidor que está a sofrer o scraping bloqueie o nosso IP.
- *Tipagem Ficheiros Models*: Os atributos nas classes `models/` têm de bater certo com os declarados na App Android. Qualquer refactoring a nomes de atributos, deverá ser feito em ambos os projetos sob pena de a App Android apresentar UI sem dados (`null`).

View File

@@ -0,0 +1,35 @@
# 03 - Projeto Android
O projeto principal que reside na pasta base do repositório é uma aplicação nativa Android em Java (`VdcScore_Project/VdcScore`). Esta é a interface visual que disponibiliza todos os dados extraídos pelo sistema ao utilizador final.
## Arquitetura Geral
O projeto Android adota uma arquitetura clássica recomendada pela Google baseada em pacotes funcionais com navegação moderna através do Navigation Component.
A raiz do código localiza-se em `app/src/main/java/com/example/vdcscore/`:
- **`models/`**: As classes de dados principais (`Club`, `Game`, `Jornada`, `Player`). Estes POJOs *(Plain Old Java Objects)* devem corresponder rigorosamente aos modelos utilizados no projeto Scraper, para que as bibliotecas do Firebase consigam realizar a conversão JSON-para-Objeto (`getValue(Model.class)`) automaticamente e sem falhas.
- **`ui/`**: Divide-se em vários subpacotes conforme as áreas da aplicação (Fragmentos e ViewModels associados). Encontramos pacotes como `clubs`, `definicoes`, `gallery`, `home`, `livegames`.
- **Raiz de Autenticação**: Ficheiros como `LoginActivity.java`, `CriarContaActivity.java`, `RecuperarPasswordActivity.java`, e a Activity principal de entrada do Navigation Graph (`MainActivity.java`).
## Tecnologias e Bibliotecas
A lista de dependências completas pode ser consultada em `app/build.gradle.kts`, destacando-se as seguintes bibliotecas chave:
- **ViewBinding**: Utilizado para ligação segura das views (elementos XML de layout) diretamente ao código Java, eliminando o antigo e verboso `findViewById()`.
- **Glide (`com.github.bumptech.glide:glide`)**: Responsável pelo carregamento, caching e exibição fluída de imagens remetidas por links, incluindo Logos dos clubes.
- **Firebase Auth (`libs.firebase.auth`)**: Gere o login e registo na plataforma.
- **Firebase Realtime Database (`libs.firebase.database`)**: Biblioteca principal para sincronismo de dados de leitura instantânea providenciados pelo scraper.
- **AndroidX Navigation Component**: Gere todo o fluxo de trocas de ecrã (Fragments) através do nav_graph da MainActivity.
## Componentes UI Essenciais
Muitos dos ecrãs (como listas de jogos ou classificações) funcionam à base de `RecyclerView`. Para cada lista, é criado um "Adapter" e um layout XML específico ("item layout").
## Interação com a Base de Dados
Em vários pontos (ViewModels ou diretamente nos Fragments), encontram-se instâncias de `ValueEventListener` anexadas a pontos de referência específicos do Firebase (ex: `DatabaseReference ref = FirebaseDatabase.getInstance().getReference("Senior").child("standings");`).
Toda a atualização da UI é ativada dentro do callback `onDataChange(DataSnapshot snapshot)`.
## Notas Importantes para IA
> [!WARNING]
> Quando pedirem a um Agente de IA para alterar a UI e os cartões de exibição de dados: O Agente deverá SEMPRE verificar e ler primeiro os atributos na classe pertencente ao pacote `models` (ex: `models/Game.java`) para saber exatamente como deve fazer o set/get dos valores nas Views. Não deve inventar getters que não existam ou pressupor que a Firebase tem chaves diferentes.

View File

@@ -0,0 +1,74 @@
# 04 - Schema da Base de Dados (Firebase Realtime Database)
Este projeto utiliza o Firebase Realtime Database. Como se trata de uma base de dados NoSQL estruturada em árvore JSON, a integridade é mantida pela concordância entre o que o **Scraper Java escreve** e aquilo que a **App Android espera ler**.
A raiz da base de dados encontra-se geralmente dividida por categoria de campeonato. Assumindo como base o desenvolvimento, a organização atual reflete a estrutura de Escalões (ex. "Senior", "Junior", etc).
## Estrutura Árvore JSON (Schema Esperado)
```json
{
"Senior": {
"standings": {
"Vila Chã": {
"clubName": "Vila Chã",
"clubLogo": "https://url...",
"points": "45",
"gamesPlayed": "15",
"wins": "15",
"draws": "0",
"losses": "0",
"goalsFor": "50",
"goalsAgainst": "10",
"position": 1
},
"Outro Clube": {
...
}
},
"journeys": {
"Jornada 1": {
"games": [
{
"homeTeam": "Clube A",
"homeLogo": "https://url...",
"awayTeam": "Clube B",
"awayLogo": "https://url...",
"homeScore": "2",
"awayScore": "1",
"date": "10-10-2023",
"field": "Campo do Clube A",
"matchReportUrl": "https://url..."
}
]
}
},
"players": {
"Vila Chã": [
{
"name": "João Silva",
"number": "10",
"position": "Avançado",
"photoUrl": "https://url..."
}
]
}
},
"Users": {
"UID_12345": {
"email": "user@email.com",
"favoriteClub": "Vila Chã"
}
}
}
```
## Regras e Convenções
- **As chaves principais** (`standings`, `journeys`, `players`) devem ser escritas com letra minúscula, facilitando as queries.
- **Formatação Numérica**: Ocasionalmente no processo de Web Scraping, os valores numéricos são guardados e transmitidos como `String`. Na App Android, ao efetuar lógicas matemáticas é importante fazer parse para `Integer`. As classes `models/` ditam o tipo final.
- **Campos Opcionais**: Determinados campos como `matchReportUrl` podem vir vazios (`""`) da AFAVCD. Os adaptadores Android deverão estar preparados para ocultar botões se essa string for nula ou vazia.
## Segurança
As regras da Realtime Database (`rules.json` na consola da Firebase) deverão espelhar a permissão "Leitura de qualquer utilizador, Escrita apenas por Admin". Isto previne que um utilizador malicioso com App Adulterada envie resultados falsos.

View File

@@ -0,0 +1,96 @@
# 05 - Progresso e Estado Atual
> [!IMPORTANT]
> **Atenção IA (Agentes):** Este documento é um Registo de Progresso (Living Document). É obrigação de qualquer agente de IA atualizar este ficheiro quando terminar um conjunto de tarefas com sucesso, ou caso depare-se com impedimentos, de forma a passar contexto às próximas iterações.
## O que já foi alcançado (Abril 2026)
O projeto sofreu um esforço significativo na estabilização do **Scraper** e da interface de utilizador (UI) na **App Android**.
### No Scraper (Java)
- Foi configurada a extração das Tabelas Classificativas (Standings) e cálculos otimizados para garantir dados precisos baseados na AFAVCD.
- Alterou-se o endpoint de single-matchday para aceder a todo o calendário de época via `/jorneys` (plural endpoint).
- O Scraper está apto a recolher não apenas golos, mas todos os campos vitais de um `Game` (Jornada, Data, Campo de Jogo e URL do Relatório), enviando-os corretamente para o Firebase.
- Iniciaram-se trabalhos nos perfis dos jogadores (PlayersScraper), com mapeamento e sincronização em curso.
- A Estrutura da class `GameMatch.java` foi mantida inalterada de forma propositada durante o refactoring, para não quebrar a compatibilidade com a schema na base de dados antiga.
### Na App Android (VdcScore)
- Interface de "Jornadas" (Matchday Display UI) revista: Desenvolvidos cartões com visual premium (Card-style).
- O Data Binding no `MatchesAdapter` foi resolvido para corretamente mapear e mostrar: nomes das equipas, logótipos descarregados com `Glide`, os resultados (scores) e a informação de agendamento do jogo (Data/Campo).
- A Autenticação de base (Logins e Registos) está implementada usando o Firebase Authentication.
## Tarefas A Decorrer / Próximos Passos (TODOs)
- **Testes Finais de UI:** Garantir que quando campos opcionais do Scraper regressam a `null` (e.g. jogos ainda não marcados não têm Data), a interface Android se comporta silenciosamente (sem Crashes e apresentando estado vazio "A Definir").
- **PlayersScraper:** Completar e validar a ingestão do plantel de todos os clubes para exibir no Menu de "Equipas".
- **Sistema Offline:** Melhorar a experiência da App permitindo o Firebase cache persistir localmente quando não há acesso à Internet.
- **Push Notifications:** Quando um Scraper deteta um fim de jogo (mudança de estado para 'Terminado'), explorar a hipótese de chamar Firebase Cloud Messaging para notificar a App Android.
## Relatório de Intervenção (Desenvolvimento do Scraper de Jornadas)
**Progresso Geral Atualizado**
Foi implementada a funcionalidade para recolher e sincronizar detalhadamente todas as jornadas e jogos correspondentes. O scraper agora processa todas as informações de jornadas da API da AFAVCD e insere esses dados na Firebase Realtime Database na respetiva estrutura (`jornadas/{escalao}/{id_jornada}/{id_jogo}`), alimentando os ecrãs da App Android.
**O que foi criado ou adicionado**
- Adicionada lógica de extração e formatação ao `StandingsScraper.java` para sincronizar as jornadas com a Firebase.
- Inclusão dos campos de cada jogo com compatibilidade direta com a classe `Match.java` da App Android (`home_nome`, `away_nome`, `home_logo`, `away_logo`, `home_golos`, `away_golos`, `data`, `hora`, `campo`, `matchReportUrl`).
**O que foi modificado e porquê**
- Modificou-se o ficheiro `StandingsScraper.java` para reaproveitar a chamada de rede que já estava a ser feita ao endpoint `/jorneys`. Optou-se por introduzir a lógica neste ficheiro, pois ele já contém o mapeamento de clubes (`clubesMap`) necessário para buscar nomes e logótipos através dos IDs das equipas (`homeId`, `awayId`).
**O que foi removido**
- Nenhuma funcionalidade ou ficheiro foi removido nesta intervenção. Apenas foi expandida a capacidade do código já existente.
## Relatório de Intervenção (Desenvolvimento do Scraper de Melhores Marcadores)
**Progresso Geral Atualizado**
Foi implementada a funcionalidade para extrair e sincronizar a lista de melhores marcadores (Top Scorers) para os escalões de Seniores e Juniores. Conseguimos identificar o endpoint de "disciplina" da AFAVCD que contém os golos marcados por cada jogador em cada jornada, permitindo calcular o total acumulado.
**O que foi criado ou adicionado**
- **Novo Modelo:** Criado `TopScorer.java` no projeto Scraper para espelhar a estrutura esperada pela App Android.
- **Novo Scraper:** Criado `TopScorersScraper.java` que:
- Identifica e acede ao endpoint: `https://api.afavcd.pt/teams/modality/{id}/season/33/discipline`.
- Soma os golos de cada jogador através de todas as jornadas.
- Faz o mapeamento automático para o nome e logo do clube usando o ID da equipa.
- Ordena os marcadores por número de golos.
- Sincroniza os dados com o Firebase em `marcadores/{escalao}`.
**O que foi modificado e porquê**
- A estrutura de classes do projeto Scraper foi expandida para incluir modelos de dados mais granulares (TopScorer), facilitando a manutenção e a paridade com o projeto Android.
**O que foi removido**
- Nenhuma funcionalidade foi removida.
## Relatório de Intervenção (UI das Jornadas - App Android)
**Progresso Geral Atualizado**
As jornadas agora são carregadas e exibidas corretamente e de forma ordenada na aplicação Android (Ecrã Jornadas / GalleryFragment). A visualização dos detalhes dos jogos foi também enriquecida permitindo o acesso à "Ficha de Jogo" oficial quando o link é fornecido pelo Scraper.
**O que foi criado ou adicionado**
- Adicionado o botão "Ficha de Jogo" no layout `item_match.xml` dos cartões de jogo.
- Implementada a propriedade `matchReportUrl` (e respetivos getters/setters) no Model `Match.java` garantindo a correspondência `@PropertyName` com os dados guardados na Firebase.
- Criada a intenção (Intent) no `MatchesAdapter.java` para abrir o browser nativo e consultar o relatório da partida.
**O que foi modificado e porquê**
- O `GalleryFragment.java` foi modificado para ordenar as jornadas `matchdaysList` de forma numérica (`Collections.sort`). Isto resolveu o problema em que o Firebase devolvia as chaves ordenadas de forma alfabética (1, 10, 11, 2, 3...) baralhando a navegação sequencial no ecrã.
- O `MatchesAdapter.java` foi modificado para suportar a alternância de visibilidade do novo botão de "Ficha de Jogo" consoante a disponibilidade do URL na base de dados.
**O que foi removido**
- Nenhuma funcionalidade ou ficheiro foi removido nesta iteração, focando-se unicamente em enriquecer a experiência do utilizador.
## Relatório de Intervenção (Ecrã de Melhores Marcadores - App Android)
**Progresso Geral Atualizado**
Foi criada toda a infraestrutura base e a interface visual para acomodar os "Melhores Marcadores" da liga (Top Scorers). O ecrã foi integrado na navegação principal da App e está desenhado para alternar rapidamente entre Seniores e Juniores. Está agora perfeitamente alinhado com a árvore do Firebase `melhores_marcadores/{escalao}`, aguardando que o Scraper Java inicie a injeção de dados.
**O que foi criado ou adicionado**
- Novo Model `TopScorer.java` com as propriedades exatas esperadas (`playerName`, `playerPhoto`, `clubName`, `clubLogo`, `goals`, `position`).
- Interface de layout (`fragment_top_scorers.xml` e `item_top_scorer.xml`) com um design em formato de cartões premium, suportando a exibição da posição do jogador, foto circular do perfil, logótipo do clube e a contagem de golos.
- `TopScorersFragment.java` e `TopScorersAdapter.java` que tratam a lógica de escuta em tempo real do Firebase e fazem a ordenação pela quantidade de golos de forma descendente.
- Menu de navegação foi alargado (`mobile_navigation.xml`, `activity_main_drawer.xml`, `MainActivity.java` e `strings.xml`) para incluir a opção lateral visível e interativa "Melhores Marcadores".
**O que foi modificado e porquê**
- Adicionado ao ficheiro `themes.xml` o estilo auxiliar `ShapeAppearanceOverlay.App.CornerSize50Percent` para garantir que as fotos dos jogadores (`ShapeableImageView`) fiquem perfeitamente circulares sem recurso a bibliotecas externas complexas.
**O que foi removido**
- Nenhuma funcionalidade removida. O código consiste numa extensão (feature) 100% nova.