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