From 85f1a3679f9ee8537d682a8803e59cebe4410d99 Mon Sep 17 00:00:00 2001 From: 230421 <230421@epvc.pt> Date: Tue, 28 Apr 2026 17:11:58 +0100 Subject: [PATCH] noticias funcionais --- .../com/example/vdcscore/MainActivity.java | 2 +- .../com/example/vdcscore/models/News.java | 62 +++++++++ .../example/vdcscore/ui/news/NewsAdapter.java | 117 +++++++++++++++++ .../vdcscore/ui/news/NewsDetailFragment.java | 120 +++++++++++++++++ .../vdcscore/ui/news/NewsFragment.java | 76 +++++++++++ app/src/main/res/layout/fragment_news.xml | 22 ++++ .../main/res/layout/fragment_news_detail.xml | 123 ++++++++++++++++++ app/src/main/res/layout/item_news.xml | 75 +++++++++++ .../main/res/menu/activity_main_drawer.xml | 4 + .../main/res/navigation/mobile_navigation.xml | 25 +++- app/src/main/res/values/strings.xml | 1 + docs/05_PROGRESSO_E_ESTADO_ATUAL.md | 43 ++++++ 12 files changed, 668 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/example/vdcscore/models/News.java create mode 100644 app/src/main/java/com/example/vdcscore/ui/news/NewsAdapter.java create mode 100644 app/src/main/java/com/example/vdcscore/ui/news/NewsDetailFragment.java create mode 100644 app/src/main/java/com/example/vdcscore/ui/news/NewsFragment.java create mode 100644 app/src/main/res/layout/fragment_news.xml create mode 100644 app/src/main/res/layout/fragment_news_detail.xml create mode 100644 app/src/main/res/layout/item_news.xml diff --git a/app/src/main/java/com/example/vdcscore/MainActivity.java b/app/src/main/java/com/example/vdcscore/MainActivity.java index f82e127..a39d15d 100644 --- a/app/src/main/java/com/example/vdcscore/MainActivity.java +++ b/app/src/main/java/com/example/vdcscore/MainActivity.java @@ -70,7 +70,7 @@ public class MainActivity extends AppCompatActivity { // menu should be considered as top level destinations. mAppBarConfiguration = new AppBarConfiguration.Builder( R.id.nav_home, R.id.nav_gallery, R.id.nav_definicoes, - R.id.nav_live_games, R.id.nav_clubs, R.id.nav_top_scorers) + R.id.nav_live_games, R.id.nav_clubs, R.id.nav_top_scorers, R.id.nav_news) .setOpenableLayout(drawer) .build(); NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main); diff --git a/app/src/main/java/com/example/vdcscore/models/News.java b/app/src/main/java/com/example/vdcscore/models/News.java new file mode 100644 index 0000000..b76d50a --- /dev/null +++ b/app/src/main/java/com/example/vdcscore/models/News.java @@ -0,0 +1,62 @@ +package com.example.vdcscore.models; + +import com.google.firebase.database.PropertyName; + +import java.io.Serializable; + +public class News implements Serializable { + private int newsID; + private String title; + private String body; + private String insertDate; + private String photoURL; + + public News() { + } + + @PropertyName("newsID") + public int getNewsID() { + return newsID; + } + + @PropertyName("newsID") + public void setNewsID(int newsID) { + this.newsID = newsID; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + @PropertyName("insertDate") + public String getInsertDate() { + return insertDate; + } + + @PropertyName("insertDate") + public void setInsertDate(String insertDate) { + this.insertDate = insertDate; + } + + @PropertyName("photoURL") + public String getPhotoURL() { + return photoURL; + } + + @PropertyName("photoURL") + public void setPhotoURL(String photoURL) { + this.photoURL = photoURL; + } +} diff --git a/app/src/main/java/com/example/vdcscore/ui/news/NewsAdapter.java b/app/src/main/java/com/example/vdcscore/ui/news/NewsAdapter.java new file mode 100644 index 0000000..0264add --- /dev/null +++ b/app/src/main/java/com/example/vdcscore/ui/news/NewsAdapter.java @@ -0,0 +1,117 @@ +package com.example.vdcscore.ui.news; + +import android.os.Build; +import android.text.Html; +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.News; + +import java.util.ArrayList; +import java.util.List; + +public class NewsAdapter extends RecyclerView.Adapter { + + private List newsList = new ArrayList<>(); + private OnNewsClickListener listener; + + public interface OnNewsClickListener { + void onNewsClick(News news, int position); + } + + public void setOnNewsClickListener(OnNewsClickListener listener) { + this.listener = listener; + } + + public void setNewsList(List newsList) { + this.newsList = (newsList != null) ? newsList : new ArrayList<>(); + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_news, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + News news = newsList.get(position); + + holder.textTitle.setText(news.getTitle()); + + // Tratar o corpo da notícia (remover HTML) + if (news.getBody() != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + holder.textBody.setText(Html.fromHtml(news.getBody(), Html.FROM_HTML_MODE_COMPACT).toString().trim()); + } else { + holder.textBody.setText(Html.fromHtml(news.getBody()).toString().trim()); + } + } + + // Formatar data se necessário (assumindo que vem da API/Firebase como ISO ou similar) + String dateStr = news.getInsertDate(); + if (dateStr != null && dateStr.length() >= 10) { + try { + // Tentar extrair "2026-04-20" do formato "2026-04-20T20:05:01.000Z" + java.text.SimpleDateFormat inputFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.getDefault()); + java.util.Date date = inputFormat.parse(dateStr); + if (date != null) { + java.text.SimpleDateFormat outputFormat = new java.text.SimpleDateFormat("dd MMM yyyy", new java.util.Locale("pt", "PT")); + dateStr = outputFormat.format(date); + } + } catch (Exception e) { + // Fallback para exibir apenas a parte da data "YYYY-MM-DD" + if(dateStr.contains("T")) { + dateStr = dateStr.split("T")[0]; + } + } + } + holder.textDate.setText(dateStr); + + Glide.with(holder.itemView.getContext()) + .load(news.getPhotoURL()) + .placeholder(R.drawable.ic_menu_gallery) + .error(R.drawable.ic_menu_gallery) + .into(holder.imgNews); + + holder.itemView.setOnClickListener(v -> { + if (listener != null) { + int pos = holder.getAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) { + listener.onNewsClick(news, pos); + } + } + }); + } + + @Override + public int getItemCount() { + return newsList.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public final ImageView imgNews; + public final TextView textTitle; + public final TextView textBody; + public final TextView textDate; + + public ViewHolder(View view) { + super(view); + imgNews = view.findViewById(R.id.img_news); + textTitle = view.findViewById(R.id.text_news_title); + textBody = view.findViewById(R.id.text_news_body); + textDate = view.findViewById(R.id.text_news_date); + } + } +} diff --git a/app/src/main/java/com/example/vdcscore/ui/news/NewsDetailFragment.java b/app/src/main/java/com/example/vdcscore/ui/news/NewsDetailFragment.java new file mode 100644 index 0000000..a564bb1 --- /dev/null +++ b/app/src/main/java/com/example/vdcscore/ui/news/NewsDetailFragment.java @@ -0,0 +1,120 @@ +package com.example.vdcscore.ui.news; + +import android.os.Build; +import android.os.Bundle; +import android.text.Html; +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.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.bumptech.glide.Glide; +import com.example.vdcscore.R; +import com.example.vdcscore.models.News; + +public class NewsDetailFragment extends Fragment { + + private ImageView imgNewsDetail; + private TextView textDate, textTitle, textBody; + private com.google.android.material.button.MaterialButton btnPrev, btnNext; + private java.util.List newsList; + private int currentIndex; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_news_detail, container, false); + + imgNewsDetail = root.findViewById(R.id.img_news_detail); + textDate = root.findViewById(R.id.text_news_detail_date); + textTitle = root.findViewById(R.id.text_news_detail_title); + textBody = root.findViewById(R.id.text_news_detail_body); + btnPrev = root.findViewById(R.id.btn_prev_news); + btnNext = root.findViewById(R.id.btn_next_news); + + if (getArguments() != null) { + News[] newsArray = (News[]) getArguments().getSerializable("news_list"); + if (newsArray != null) { + newsList = java.util.Arrays.asList(newsArray); + } + currentIndex = getArguments().getInt("news_index", 0); + + if (newsList != null && currentIndex >= 0 && currentIndex < newsList.size()) { + updateUI(); + } + } + + btnPrev.setOnClickListener(v -> { + if (newsList != null && currentIndex > 0) { + currentIndex--; + updateUI(); + } + }); + + btnNext.setOnClickListener(v -> { + if (newsList != null && currentIndex < newsList.size() - 1) { + currentIndex++; + updateUI(); + } + }); + + return root; + } + + private void updateUI() { + if (newsList == null || currentIndex < 0 || currentIndex >= newsList.size()) return; + + News news = newsList.get(currentIndex); + populateUI(news); + + // Update button states + btnPrev.setEnabled(currentIndex > 0); + btnNext.setEnabled(currentIndex < newsList.size() - 1); + + // Reset scroll position if needed + View scroll = getView() != null ? getView().findViewById(R.id.scroll_detail) : null; + if (scroll != null) { + scroll.scrollTo(0, 0); + } + } + + private void populateUI(News news) { + textTitle.setText(news.getTitle()); + + if (news.getBody() != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + textBody.setText(Html.fromHtml(news.getBody(), Html.FROM_HTML_MODE_COMPACT).toString().trim()); + } else { + textBody.setText(Html.fromHtml(news.getBody()).toString().trim()); + } + } + + String dateStr = news.getInsertDate(); + if (dateStr != null && dateStr.length() >= 10) { + try { + java.text.SimpleDateFormat inputFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.getDefault()); + java.util.Date date = inputFormat.parse(dateStr); + if (date != null) { + java.text.SimpleDateFormat outputFormat = new java.text.SimpleDateFormat("dd MMM yyyy", new java.util.Locale("pt", "PT")); + dateStr = outputFormat.format(date); + } + } catch (Exception e) { + if(dateStr.contains("T")) { + dateStr = dateStr.split("T")[0]; + } + } + } + textDate.setText(dateStr); + + Glide.with(this) + .load(news.getPhotoURL()) + .placeholder(R.drawable.ic_menu_gallery) + .error(R.drawable.ic_menu_gallery) + .into(imgNewsDetail); + } +} diff --git a/app/src/main/java/com/example/vdcscore/ui/news/NewsFragment.java b/app/src/main/java/com/example/vdcscore/ui/news/NewsFragment.java new file mode 100644 index 0000000..a120265 --- /dev/null +++ b/app/src/main/java/com/example/vdcscore/ui/news/NewsFragment.java @@ -0,0 +1,76 @@ +package com.example.vdcscore.ui.news; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +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.News; +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.List; + +public class NewsFragment extends Fragment { + + private RecyclerView recyclerView; + private NewsAdapter adapter; + private DatabaseReference databaseReference; + private List newsList = new ArrayList<>(); + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_news, container, false); + + recyclerView = root.findViewById(R.id.recycler_news); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + adapter = new NewsAdapter(); + adapter.setOnNewsClickListener((news, position) -> { + Bundle bundle = new Bundle(); + bundle.putSerializable("news_list", newsList.toArray(new News[0])); + bundle.putInt("news_index", position); + androidx.navigation.Navigation.findNavController(root).navigate(R.id.action_nav_news_to_nav_news_detail, bundle); + }); + recyclerView.setAdapter(adapter); + + databaseReference = FirebaseDatabase.getInstance().getReference("noticias"); + + loadNews(); + + return root; + } + + private void loadNews() { + databaseReference.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot snapshot) { + newsList.clear(); + for (DataSnapshot dataSnapshot : snapshot.getChildren()) { + News news = dataSnapshot.getValue(News.class); + if (news != null) { + newsList.add(news); + } + } + adapter.setNewsList(newsList); + } + + @Override + public void onCancelled(@NonNull DatabaseError error) { + if (getContext() != null) { + Toast.makeText(getContext(), "Erro ao carregar notícias", Toast.LENGTH_SHORT).show(); + } + } + }); + } +} diff --git a/app/src/main/res/layout/fragment_news.xml b/app/src/main/res/layout/fragment_news.xml new file mode 100644 index 0000000..870c9c6 --- /dev/null +++ b/app/src/main/res/layout/fragment_news.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_news_detail.xml b/app/src/main/res/layout/fragment_news_detail.xml new file mode 100644 index 0000000..c617374 --- /dev/null +++ b/app/src/main/res/layout/fragment_news_detail.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_news.xml b/app/src/main/res/layout/item_news.xml new file mode 100644 index 0000000..8306202 --- /dev/null +++ b/app/src/main/res/layout/item_news.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml index 840f1ae..a59cafe 100644 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -4,6 +4,10 @@ tools:showIn="navigation_view"> + + app:startDestination="@+id/nav_news"> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e9bc29..9c43493 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,6 +15,7 @@ Detalhes do Jogo Clubes Melhores Marcadores + Ínicio Alterar Foto de Perfil diff --git a/docs/05_PROGRESSO_E_ESTADO_ATUAL.md b/docs/05_PROGRESSO_E_ESTADO_ATUAL.md index 21b6862..0c9f39a 100644 --- a/docs/05_PROGRESSO_E_ESTADO_ATUAL.md +++ b/docs/05_PROGRESSO_E_ESTADO_ATUAL.md @@ -94,3 +94,46 @@ Foi criada toda a infraestrutura base e a interface visual para acomodar os "Mel **O que foi removido** - Nenhuma funcionalidade removida. O código consiste numa extensão (feature) 100% nova. + +## Relatório de Intervenção (Implementação da Secção de Notícias) + +**Progresso Geral Atualizado** +Foi implementado um novo sistema de notícias que permite à app exibir as novidades mais recentes da AFAVCD. O sistema é composto por um scraper que recolhe os dados da API oficial e uma nova interface na app Android que apresenta as notícias num formato de resumo visualmente apelativo (cards), seguindo a estética solicitada. + +**O que foi criado ou adicionado** +- **No Scraper (Java):** + - Criado o modelo `News.java`. + - Criado o `NewsScraper.java` que extrai dados de `https://api.afavcd.pt/news/1` e sincroniza-os com o nó `noticias` na Firebase. +- **Na App Android:** + - Adicionado o modelo `News.java` com mapeamento para os campos da Firebase. + - Desenvolvido o layout `item_news.xml` com um design premium (Imagens de destaque, títulos em cyan e resumos limpos). + - Criado o `fragment_news.xml` com um cabeçalho personalizado e uma `RecyclerView`. + - Implementados o `NewsAdapter.java` (com limpeza de tags HTML do corpo da notícia) e o `NewsFragment.java`. + - Integrada a nova secção no menu lateral (`activity_main_drawer.xml`), no grafo de navegação (`mobile_navigation.xml`) e configurada como destino de topo na `MainActivity.java`. + +**O que foi modificado e porquê** +- `strings.xml`: Adicionada a label "Notícias" para internacionalização e uso no menu. +- `MainActivity.java`: Atualizada a `AppBarConfiguration` para incluir o novo fragmento de notícias, garantindo que o menu lateral esteja disponível em vez do botão de retrocesso. +- `mobile_navigation.xml` e `activity_main_drawer.xml`: Atualizados para permitir a navegação para a nova funcionalidade. + +**O que foi removido** +- Nenhuma funcionalidade foi removida. O projeto foi expandido com uma nova feature. + +## Relatório de Intervenção (Ecrã de Ínicio / Notícias) + +**Progresso Geral Atualizado** +As notícias foram promovidas a ecrã principal (Ínicio) da aplicação. A secção de notícias é agora a primeira a ser apresentada quando o utilizador abre a app e o ecrã foi sujeito a uma profunda revisão estética para apresentar uma leitura mais imersiva, moderna e limpa (Cards formatados). + +**O que foi criado ou adicionado** +- Nenhuma class nova foi criada. O foco foi estritamente na reorganização e melhoria de UI e UX. +- Adicionada lógica de parsing e formatação da data no `NewsAdapter.java` para converter strings ISO complexas (`"2026-04-20T20:05:01.000Z"`) em datas humanamente legíveis e agradáveis (e.g. `20 abr 2026`). + +**O que foi modificado e porquê** +- `strings.xml`: A string `@string/menu_news` foi alterada de "Notícias" para "Ínicio" para corresponder à semântica desejada pelo utilizador para o ecrã de entrada. +- `mobile_navigation.xml`: O `startDestination` foi alterado de `nav_home` (Classificações) para `nav_news` (Ínicio/Notícias) para garantir que a app abre logo nas notícias recentes. +- `activity_main_drawer.xml`: O item `nav_news` foi movido para o topo do menu para respeitar a hierarquia visual. +- `fragment_news.xml`: Removido o cabeçalho cyan ("Notícias"), integrando perfeitamente a lista com a barra de ferramentas nativa da aplicação Android. +- `item_news.xml`: Completamente redesenhado. Adicionado um layout `MaterialCardView` com cantos mais arredondados, margens e sombras otimizadas, e melhorada drasticamente a hierarquia tipográfica (cores neutras premium `#1F2937` e `#4B5563`, e a imagem a ocupar a largura total do card). + +**O que foi removido** +- O cabeçalho estático no ficheiro `fragment_news.xml` foi removido para promover um design mais nativo e espaçoso.