diff --git a/README.md b/README.md new file mode 100644 index 0000000..44a36ed --- /dev/null +++ b/README.md @@ -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`. diff --git a/app/src/main/java/com/example/vdcscore/models/TopScorer.java b/app/src/main/java/com/example/vdcscore/models/TopScorer.java new file mode 100644 index 0000000..ffad2a8 --- /dev/null +++ b/app/src/main/java/com/example/vdcscore/models/TopScorer.java @@ -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; + } +} diff --git a/app/src/main/java/com/example/vdcscore/ui/topscorers/TopScorersAdapter.java b/app/src/main/java/com/example/vdcscore/ui/topscorers/TopScorersAdapter.java new file mode 100644 index 0000000..35c3dc9 --- /dev/null +++ b/app/src/main/java/com/example/vdcscore/ui/topscorers/TopScorersAdapter.java @@ -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 { + + private List scorers = new ArrayList<>(); + + public void setTopScorers(List 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); + } + } +} diff --git a/app/src/main/java/com/example/vdcscore/ui/topscorers/TopScorersFragment.java b/app/src/main/java/com/example/vdcscore/ui/topscorers/TopScorersFragment.java new file mode 100644 index 0000000..c401c14 --- /dev/null +++ b/app/src/main/java/com/example/vdcscore/ui/topscorers/TopScorersFragment.java @@ -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 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() { + @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); + } + } +} diff --git a/app/src/main/res/drawable/bg_circle_light_gray.xml b/app/src/main/res/drawable/bg_circle_light_gray.xml new file mode 100644 index 0000000..82cd5c8 --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_light_gray.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/layout/fragment_top_scorers.xml b/app/src/main/res/layout/fragment_top_scorers.xml new file mode 100644 index 0000000..f47a79b --- /dev/null +++ b/app/src/main/res/layout/fragment_top_scorers.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_top_scorer.xml b/app/src/main/res/layout/item_top_scorer.xml new file mode 100644 index 0000000..c06ecea --- /dev/null +++ b/app/src/main/res/layout/item_top_scorer.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/01_ARQUITETURA.md b/docs/01_ARQUITETURA.md new file mode 100644 index 0000000..f7ac726 --- /dev/null +++ b/docs/01_ARQUITETURA.md @@ -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. diff --git a/docs/02_PROJETO_SCRAPER.md b/docs/02_PROJETO_SCRAPER.md new file mode 100644 index 0000000..a949cc9 --- /dev/null +++ b/docs/02_PROJETO_SCRAPER.md @@ -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`). diff --git a/docs/03_PROJETO_ANDROID.md b/docs/03_PROJETO_ANDROID.md new file mode 100644 index 0000000..1369b83 --- /dev/null +++ b/docs/03_PROJETO_ANDROID.md @@ -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. diff --git a/docs/04_SCHEMA_BASE_DADOS.md b/docs/04_SCHEMA_BASE_DADOS.md new file mode 100644 index 0000000..98fa2f7 --- /dev/null +++ b/docs/04_SCHEMA_BASE_DADOS.md @@ -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. diff --git a/docs/05_PROGRESSO_E_ESTADO_ATUAL.md b/docs/05_PROGRESSO_E_ESTADO_ATUAL.md new file mode 100644 index 0000000..21b6862 --- /dev/null +++ b/docs/05_PROGRESSO_E_ESTADO_ATUAL.md @@ -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.