melhores marcadores adicionados
This commit is contained in:
25
README.md
Normal file
25
README.md
Normal 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`.
|
||||
78
app/src/main/java/com/example/vdcscore/models/TopScorer.java
Normal file
78
app/src/main/java/com/example/vdcscore/models/TopScorer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
app/src/main/res/drawable/bg_circle_light_gray.xml
Normal file
5
app/src/main/res/drawable/bg_circle_light_gray.xml
Normal 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>
|
||||
80
app/src/main/res/layout/fragment_top_scorers.xml
Normal file
80
app/src/main/res/layout/fragment_top_scorers.xml
Normal 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>
|
||||
115
app/src/main/res/layout/item_top_scorer.xml
Normal file
115
app/src/main/res/layout/item_top_scorer.xml
Normal 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
32
docs/01_ARQUITETURA.md
Normal 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.
|
||||
30
docs/02_PROJETO_SCRAPER.md
Normal file
30
docs/02_PROJETO_SCRAPER.md
Normal 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`).
|
||||
35
docs/03_PROJETO_ANDROID.md
Normal file
35
docs/03_PROJETO_ANDROID.md
Normal 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.
|
||||
74
docs/04_SCHEMA_BASE_DADOS.md
Normal file
74
docs/04_SCHEMA_BASE_DADOS.md
Normal 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.
|
||||
96
docs/05_PROGRESSO_E_ESTADO_ATUAL.md
Normal file
96
docs/05_PROGRESSO_E_ESTADO_ATUAL.md
Normal 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.
|
||||
Reference in New Issue
Block a user