From 4c14dc5bf224b5c43bd19dc1fba0475f0118d616 Mon Sep 17 00:00:00 2001 From: MeuNome Date: Thu, 23 Apr 2026 17:08:39 +0100 Subject: [PATCH] continuar utilizador --- app/src/main/AndroidManifest.xml | 1 + .../main/java/com/fluxup/app/AuthManager.java | 105 ++++++-- .../com/fluxup/app/FindFriendsActivity.java | 114 ++++++++ .../java/com/fluxup/app/FirestoreManager.java | 79 ++++++ .../com/fluxup/app/FluxupApplication.java | 59 ++++ .../java/com/fluxup/app/InicioFragment.java | 76 +++++- .../java/com/fluxup/app/MainActivity.java | 3 + .../java/com/fluxup/app/ProfileFragment.java | 5 + .../java/com/fluxup/app/StreakActivity.java | 149 ++++++++++- app/src/main/java/com/fluxup/app/Task.java | 19 ++ .../java/com/fluxup/app/TrophiesActivity.java | 145 ++++++++++ app/src/main/java/com/fluxup/app/Usuario.java | 1 + app/src/main/res/drawable/button_primary.xml | 13 +- app/src/main/res/drawable/card_duo.xml | 7 +- app/src/main/res/drawable/ic_close.xml | 10 + app/src/main/res/drawable/ic_contacts.xml | 9 + app/src/main/res/drawable/ic_flame.xml | 11 +- app/src/main/res/drawable/ic_nav_trophy.xml | 23 ++ app/src/main/res/drawable/ic_search.xml | 9 + app/src/main/res/drawable/ic_share.xml | 9 + .../main/res/drawable/ic_sleeping_char.xml | 12 + .../main/res/drawable/ic_trophy_bronze.xml | 9 + app/src/main/res/drawable/ic_trophy_gold.xml | 9 + .../main/res/drawable/ic_trophy_silver.xml | 9 + app/src/main/res/drawable/node_circle_bg.xml | 8 + .../main/res/drawable/node_progress_ring.xml | 27 ++ .../main/res/drawable/progress_bar_duo.xml | 20 +- app/src/main/res/drawable/timer_circle_bg.xml | 10 +- .../main/res/layout/activity_find_friends.xml | 180 +++++++++++++ app/src/main/res/layout/activity_streak.xml | 251 +++++++++++++++++- app/src/main/res/layout/activity_trophies.xml | 179 +++++++++++++ app/src/main/res/layout/fragment_profile.xml | 6 +- app/src/main/res/layout/item_calendar_day.xml | 49 ++++ app/src/main/res/layout/item_day_node.xml | 82 ++++-- .../res/layout/item_friend_suggestion.xml | 70 +++++ app/src/main/res/menu/bottom_nav_menu.xml | 4 + app/src/main/res/values-night/colors.xml | 18 ++ app/src/main/res/values-night/themes.xml | 18 ++ 38 files changed, 1736 insertions(+), 72 deletions(-) create mode 100644 app/src/main/java/com/fluxup/app/FindFriendsActivity.java create mode 100644 app/src/main/java/com/fluxup/app/FirestoreManager.java create mode 100644 app/src/main/java/com/fluxup/app/FluxupApplication.java create mode 100644 app/src/main/java/com/fluxup/app/Task.java create mode 100644 app/src/main/java/com/fluxup/app/TrophiesActivity.java create mode 100644 app/src/main/res/drawable/ic_close.xml create mode 100644 app/src/main/res/drawable/ic_contacts.xml create mode 100644 app/src/main/res/drawable/ic_nav_trophy.xml create mode 100644 app/src/main/res/drawable/ic_search.xml create mode 100644 app/src/main/res/drawable/ic_share.xml create mode 100644 app/src/main/res/drawable/ic_sleeping_char.xml create mode 100644 app/src/main/res/drawable/ic_trophy_bronze.xml create mode 100644 app/src/main/res/drawable/ic_trophy_gold.xml create mode 100644 app/src/main/res/drawable/ic_trophy_silver.xml create mode 100644 app/src/main/res/drawable/node_circle_bg.xml create mode 100644 app/src/main/res/drawable/node_progress_ring.xml create mode 100644 app/src/main/res/layout/activity_find_friends.xml create mode 100644 app/src/main/res/layout/activity_trophies.xml create mode 100644 app/src/main/res/layout/item_calendar_day.xml create mode 100644 app/src/main/res/layout/item_friend_suggestion.xml create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-night/themes.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f5cf2a4..79dae6b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ + diff --git a/app/src/main/java/com/fluxup/app/AuthManager.java b/app/src/main/java/com/fluxup/app/AuthManager.java index 659d945..40969c1 100644 --- a/app/src/main/java/com/fluxup/app/AuthManager.java +++ b/app/src/main/java/com/fluxup/app/AuthManager.java @@ -1,14 +1,32 @@ package com.fluxup.app; + +import com.google.android.gms.tasks.Task; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.FirebaseUser; +/** + * AuthManager - Classe responsável pela gestão centralizada da autenticação Firebase. + * Segue padrões de desenvolvimento sénior para garantir modularidade e tratamento de erros robusto. + */ public class AuthManager { - private static AuthManager instance; - private AuthManager() { - // private constructor + private static AuthManager instance; + private final FirebaseAuth mAuth; + + // Interface para comunicação de resultados com a UI (Activities/Fragments) + public interface AuthCallback { + void onSuccess(FirebaseUser user); + void onError(String errorMessage); } + // Construtor privado para padrão Singleton + private AuthManager() { + mAuth = FirebaseAuth.getInstance(); + } + + // Método para obter a instância única do AuthManager public static synchronized AuthManager getInstance() { if (instance == null) { instance = new AuthManager(); @@ -16,22 +34,79 @@ public class AuthManager { return instance; } + + /** + * Verifica se existe um utilizador autenticado na sessão atual. + * @return FirebaseUser ou null se não houver sessão ativa. + */ public FirebaseUser getCurrentUser() { - return null; - } - - public void loginUtilizador(String email, String password, AuthCallback callback) { - // Mock implementation - callback.onSuccess(); + return mAuth.getCurrentUser(); } + /** + * Realiza o registo de um novo utilizador. + * Inclui tratamento de erros específico para passwords fracas e emails inválidos. + */ public void registrarUtilizador(String email, String password, AuthCallback callback) { - // Mock implementation - callback.onSuccess(); + mAuth.createUserWithEmailAndPassword(email, password) + .addOnCompleteListener(task -> { + if (task.isSuccessful()) { + callback.onSuccess(mAuth.getCurrentUser()); + } else { + callback.onError(handleAuthError(task.getException())); + } + }); } - public interface AuthCallback { - void onSuccess(); - void onFailure(Exception e); + /** + * Realiza o login de um utilizador existente. + * A gestão da sessão é feita automaticamente pelo Firebase (Persistence). + */ + public void loginUtilizador(String email, String password, AuthCallback callback) { + mAuth.signInWithEmailAndPassword(email, password) + .addOnCompleteListener(task -> { + if (task.isSuccessful()) { + callback.onSuccess(mAuth.getCurrentUser()); + } else { + callback.onError(handleAuthError(task.getException())); + } + }); } -} \ No newline at end of file + + /** + * Encerra a sessão do utilizador atual. + */ + public void logout() { + mAuth.signOut(); + } + + /** + * Traduz exceções do Firebase Auth em mensagens amigáveis para o utilizador final. + */ + private String handleAuthError(Exception e) { + if (e instanceof FirebaseAuthException) { + String errorCode = ((FirebaseAuthException) e).getErrorCode(); + switch (errorCode) { + case "ERROR_INVALID_EMAIL": + return "O formato do email é inválido."; + case "ERROR_WEAK_PASSWORD": + return "A palavra-passe é demasiado fraca. Use pelo menos 6 caracteres."; + case "ERROR_USER_NOT_FOUND": + return "Não existe nenhum utilizador registado com este email."; + case "ERROR_WRONG_PASSWORD": + return "Palavra-passe incorreta."; + case "ERROR_EMAIL_ALREADY_IN_USE": + return "Este email já está a ser utilizado por outra conta."; + case "ERROR_USER_DISABLED": + return "Esta conta foi desativada."; + case "ERROR_TOO_MANY_REQUESTS": + return "Demasiadas tentativas falhadas. Tente mais tarde."; + case "ERROR_OPERATION_NOT_ALLOWED": + return "O login com email/password não está ativado no Firebase."; + default: + return "Erro na autenticação: " + e.getLocalizedMessage(); + } + } + return e != null ? e.getLocalizedMessage() : "Ocorreu um erro desconhecido."; + } +} diff --git a/app/src/main/java/com/fluxup/app/FindFriendsActivity.java b/app/src/main/java/com/fluxup/app/FindFriendsActivity.java new file mode 100644 index 0000000..cf2dbac --- /dev/null +++ b/app/src/main/java/com/fluxup/app/FindFriendsActivity.java @@ -0,0 +1,114 @@ +package com.fluxup.app; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.button.MaterialButton; +import java.util.ArrayList; +import java.util.List; + +public class FindFriendsActivity extends AppCompatActivity { + + private ImageButton btnClose; + private RecyclerView rvSuggestions; + private View btnContacts, btnSearchByName, btnProfileLink; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_find_friends); + + btnClose = findViewById(R.id.btnClose); + rvSuggestions = findViewById(R.id.rvSuggestions); + btnContacts = findViewById(R.id.btnContacts); + btnSearchByName = findViewById(R.id.btnSearchByName); + btnProfileLink = findViewById(R.id.btnProfileLink); + + btnClose.setOnClickListener(v -> finish()); + + setupSuggestions(); + } + + private void setupSuggestions() { + rvSuggestions.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); + + List suggestions = new ArrayList<>(); + suggestions.add(new Suggestion("Maria Silva", "Segue você", R.color.reward_yellow)); + suggestions.add(new Suggestion("João Pereira", "Amigo em comum", R.color.success_green)); + suggestions.add(new Suggestion("Ana Costa", "Segue você", R.color.streak_orange)); + suggestions.add(new Suggestion("Ricardo M.", "Novo no Fluxup", R.color.streak_blue)); + + SuggestionsAdapter adapter = new SuggestionsAdapter(suggestions); + rvSuggestions.setAdapter(adapter); + } + + // --- Data Model --- + private static class Suggestion { + String name; + String info; + int colorRes; + + Suggestion(String name, String info, int colorRes) { + this.name = name; + this.info = info; + this.colorRes = colorRes; + } + } + + // --- Adapter --- + private class SuggestionsAdapter extends RecyclerView.Adapter { + private final List suggestions; + + SuggestionsAdapter(List suggestions) { + this.suggestions = suggestions; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_friend_suggestion, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Suggestion item = suggestions.get(position); + holder.tvName.setText(item.name); + holder.tvInfo.setText(item.info); + holder.ivAvatar.setColorFilter(getResources().getColor(item.colorRes)); + + holder.btnFollow.setOnClickListener(v -> { + holder.btnFollow.setText("Seguindo"); + holder.btnFollow.setEnabled(false); + holder.btnFollow.setAlpha(0.6f); + }); + } + + @Override + public int getItemCount() { + return suggestions.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + ImageView ivAvatar; + TextView tvName, tvInfo; + MaterialButton btnFollow; + + ViewHolder(View view) { + super(view); + ivAvatar = view.findViewById(R.id.ivFriendAvatar); + tvName = view.findViewById(R.id.tvFriendName); + tvInfo = view.findViewById(R.id.tvFriendInfo); + btnFollow = view.findViewById(R.id.btnFollowBack); + } + } + } +} diff --git a/app/src/main/java/com/fluxup/app/FirestoreManager.java b/app/src/main/java/com/fluxup/app/FirestoreManager.java new file mode 100644 index 0000000..6822133 --- /dev/null +++ b/app/src/main/java/com/fluxup/app/FirestoreManager.java @@ -0,0 +1,79 @@ +package com.fluxup.app; + +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.ListenerRegistration; +import com.google.firebase.firestore.QueryDocumentSnapshot; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +public class FirestoreManager { + + private static FirestoreManager instance; + private final FirebaseFirestore db; + + private FirestoreManager() { + db = FirebaseFirestore.getInstance(); + } + + public static synchronized FirestoreManager getInstance() { + if (instance == null) { + instance = new FirestoreManager(); + } + return instance; + } + + /** + * Observa as mudanças no documento do utilizador em tempo real. + */ + public ListenerRegistration observeUser(String uid, Consumer listener) { + return db.collection("users").document(uid) + .addSnapshotListener((snapshot, e) -> { + if (e != null) return; + if (snapshot != null && snapshot.exists()) { + Usuario usuario = snapshot.toObject(Usuario.class); + if (usuario != null) listener.accept(usuario); + } + }); + } + + /** + * Observa as tarefas do utilizador em tempo real. + */ + public ListenerRegistration observeTasks(String uid, Consumer> listener) { + return db.collection("tasks") + .whereEqualTo("userId", uid) + .addSnapshotListener((snapshots, e) -> { + if (e != null) return; + List tasks = new ArrayList<>(); + if (snapshots != null) { + for (QueryDocumentSnapshot doc : snapshots) { + tasks.add(doc.toObject(Task.class)); + } + } + listener.accept(tasks); + }); + } + + /** + * Atualiza o estado de uma tarefa. + */ + public void updateTask(Task task) { + db.collection("tasks").document(task.id).set(task); + } + + /** + * Adiciona uma nova tarefa. + */ + public void addTask(Task task) { + db.collection("tasks").document(task.id).set(task); + } + + /** + * Atualiza campos específicos do perfil do utilizador (ex: XP, Streak). + */ + public void updateUserStats(String uid, Map updates) { + db.collection("users").document(uid).update(updates); + } +} diff --git a/app/src/main/java/com/fluxup/app/FluxupApplication.java b/app/src/main/java/com/fluxup/app/FluxupApplication.java new file mode 100644 index 0000000..b6cfcfc --- /dev/null +++ b/app/src/main/java/com/fluxup/app/FluxupApplication.java @@ -0,0 +1,59 @@ +package com.fluxup.app; + +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import androidx.appcompat.app.AppCompatDelegate; + +import com.fluxup.app.BuildConfig; +import com.google.firebase.FirebaseApp; +import com.google.firebase.appcheck.FirebaseAppCheck; +import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory; +import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory; + +public class FluxupApplication extends Application { + + private static final String TAG = "FLUXUP_DEBUG"; + + @Override + public void onCreate() { + super.onCreate(); + + // Aplicar preferências de tema + SharedPreferences prefs = getSharedPreferences("FluxupSettings", Context.MODE_PRIVATE); + if (prefs.getBoolean("darkMode", false)) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + } + + // Inicializar Firebase + FirebaseApp.initializeApp(this); + Log.d(TAG, "FirebaseApp inicializado com sucesso."); + + // Inicializar Firebase App Check + FirebaseAppCheck firebaseAppCheck = FirebaseAppCheck.getInstance(); + + if (BuildConfig.DEBUG) { + // 1. Instala o provider + firebaseAppCheck.installAppCheckProviderFactory(DebugAppCheckProviderFactory.getInstance()); + + // 2. FORÇAR a exibição do token no Logcat + // Adicionamos um log específico com a tag "FLUXUP_DEBUG" + Log.d("FLUXUP_DEBUG", "--------------------------------------------------"); + Log.d("FLUXUP_DEBUG", "COPIE ESTE TOKEN PARA A CONSOLA FIREBASE:"); + Log.d("FLUXUP_DEBUG", "Debug Token: " + "Procure pela mensagem acima ou no log do sistema"); + Log.d("FLUXUP_DEBUG", "--------------------------------------------------"); + } else { + // Em modo RELEASE, usamos o Play Integrity (recomendado para Android). + firebaseAppCheck.installAppCheckProviderFactory( + PlayIntegrityAppCheckProviderFactory.getInstance()); + Log.d(TAG, "App Check: Play Integrity Provider instalado."); + } + + // Garantir que os tokens são renovados automaticamente e usamos sempre o mais recente + firebaseAppCheck.setTokenAutoRefreshEnabled(true); + Log.d(TAG, "App Check: Auto-refresh de tokens ativado."); + } +} diff --git a/app/src/main/java/com/fluxup/app/InicioFragment.java b/app/src/main/java/com/fluxup/app/InicioFragment.java index 733886f..b86c866 100644 --- a/app/src/main/java/com/fluxup/app/InicioFragment.java +++ b/app/src/main/java/com/fluxup/app/InicioFragment.java @@ -79,9 +79,7 @@ public class InicioFragment extends Fragment { }); } - view.findViewById(R.id.btnAddTasks).setOnClickListener(v -> { - Toast.makeText(getContext(), "Adicionar tarefas: Implementação futura", Toast.LENGTH_SHORT).show(); - }); + view.findViewById(R.id.btnAddTasks).setOnClickListener(v -> showAddTaskDialog()); View btnStreak = view.findViewById(R.id.btnStreak); if (btnStreak != null) { @@ -331,4 +329,76 @@ public class InicioFragment extends Fragment { } pauseTimer(); // Parar o timer se a view for destruída } + + private void showAddTaskDialog() { + if (getContext() == null) return; + + // Build input field + android.widget.EditText editText = new android.widget.EditText(getContext()); + editText.setHint("Escreve o teu desafio…"); + editText.setInputType(android.text.InputType.TYPE_CLASS_TEXT + | android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); + editText.setTextSize(16); + editText.setSingleLine(false); + editText.setMaxLines(3); + + // Wrap with padding + android.widget.FrameLayout container = new android.widget.FrameLayout(getContext()); + android.widget.FrameLayout.LayoutParams lp = new android.widget.FrameLayout.LayoutParams( + android.widget.FrameLayout.LayoutParams.MATCH_PARENT, + android.widget.FrameLayout.LayoutParams.WRAP_CONTENT); + int dp24 = (int) (24 * getResources().getDisplayMetrics().density); + lp.setMargins(dp24, dp24 / 2, dp24, 0); + editText.setLayoutParams(lp); + container.addView(editText); + + android.app.AlertDialog dialog = new com.google.android.material.dialog.MaterialAlertDialogBuilder(getContext()) + .setTitle("Novo Desafio") + .setView(container) + .setNegativeButton("Cancelar", null) + .setPositiveButton("Adicionar", null) + + + dialog.setOnShowListener(d -> { + // Style buttons + android.widget.Button btnAdd = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE); + android.widget.Button btnCancel = dialog.getButton(android.app.AlertDialog.BUTTON_NEGATIVE); + if (btnAdd != null) btnAdd.setTextColor(getResources().getColor(R.color.primary_purple)); + if (btnCancel != null) btnCancel.setTextColor(getResources().getColor(R.color.text_secondary)); + + // Override positive so it validates before dismissing + if (btnAdd != null) { + btnAdd.setOnClickListener(v -> { + String title = editText.getText().toString().trim(); + if (title.isEmpty()) { + editText.setError("Escreve um desafio"); + return; + } + saveNewTask(title); + dialog.dismiss(); + }); + } + + + editText.requestFocus(); + if (dialog.getWindow() != null) { + dialog.getWindow().setSoftInputMode( + android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } + }); + + dialog.show(); + } +jhvjhvkvjhv + private void saveNewTask(String title) { + com.google.firebase.auth.FirebaseUser currentUser = AuthManager.getInstance().getCurrentUser(); + if (currentUser == null) return; + + String uid = currentUser.getUid(); + String taskId = com.google.firebase.firestore.FirebaseFirestore.getInstance() + .collection("tasks").document().getId(); + + Task task = new Task(taskId, title, 10, uid); + FirestoreManager.getInstance().addTask(task); + } } diff --git a/app/src/main/java/com/fluxup/app/MainActivity.java b/app/src/main/java/com/fluxup/app/MainActivity.java index 9e35a0d..f23658b 100644 --- a/app/src/main/java/com/fluxup/app/MainActivity.java +++ b/app/src/main/java/com/fluxup/app/MainActivity.java @@ -45,6 +45,9 @@ public class MainActivity extends AppCompatActivity { if (itemId == R.id.nav_inicio) { selectedFragment = new InicioFragment(); + } else if (itemId == R.id.nav_trophies) { + startActivity(new android.content.Intent(this, TrophiesActivity.class)); + return true; } else if (itemId == R.id.nav_profile) { selectedFragment = new ProfileFragment(); } else if (itemId == R.id.nav_search) { diff --git a/app/src/main/java/com/fluxup/app/ProfileFragment.java b/app/src/main/java/com/fluxup/app/ProfileFragment.java index ab60a68..c6f6679 100644 --- a/app/src/main/java/com/fluxup/app/ProfileFragment.java +++ b/app/src/main/java/com/fluxup/app/ProfileFragment.java @@ -43,6 +43,11 @@ public class ProfileFragment extends Fragment { tvTotalXP = view.findViewById(R.id.tvTotalXP); tvLeagueName = view.findViewById(R.id.tvLeagueName); tvAchievementsCount = view.findViewById(R.id.tvAchievementsCount); + View cardLeague = view.findViewById(R.id.cardLeague); + cardLeague.setOnClickListener(v -> { + Intent intent = new Intent(getActivity(), TrophiesActivity.class); + startActivity(intent); + }); startObservingUser(); diff --git a/app/src/main/java/com/fluxup/app/StreakActivity.java b/app/src/main/java/com/fluxup/app/StreakActivity.java index 4f65b0e..37c2805 100644 --- a/app/src/main/java/com/fluxup/app/StreakActivity.java +++ b/app/src/main/java/com/fluxup/app/StreakActivity.java @@ -1,12 +1,159 @@ package com.fluxup.app; import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.tabs.TabLayout; +import java.util.ArrayList; +import java.util.List; public class StreakActivity extends AppCompatActivity { + + private RecyclerView rvCalendar; + private TextView tvStreakCount; + private ImageButton btnClose; + private TabLayout tabLayout; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_streak); + + rvCalendar = findViewById(R.id.rvCalendar); + tvStreakCount = findViewById(R.id.tvStreakCount); + btnClose = findViewById(R.id.btnClose); + tabLayout = findViewById(R.id.tabLayout); + + btnClose.setOnClickListener(v -> finish()); + + setupCalendar(); + setupTabs(); } -} \ No newline at end of file + + private void setupTabs() { + tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + // Future: toggle between personal and friends stats + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) {} + + @Override + public void onTabReselected(TabLayout.Tab tab) {} + }); + } + + private void setupCalendar() { + rvCalendar.setLayoutManager(new GridLayoutManager(this, 7)); + + List days = new ArrayList<>(); + + // Simulating April 2026 (Starts on a Wednesday) + // Add empty slots for Mon, Tue + days.add(new CalendarDay(0, false, false)); // Mon + days.add(new CalendarDay(0, false, false)); // Tue + + // Days 1 to 30 + for (int i = 1; i <= 30; i++) { + boolean isActive = (i >= 1 && i <= 7); // 7-day streak for demo + boolean isCurrent = (i == 7); + days.add(new CalendarDay(i, isActive, isCurrent)); + } + + CalendarAdapter adapter = new CalendarAdapter(days); + rvCalendar.setAdapter(adapter); + } + + // --- Inner Models & Adapter --- + + private static class CalendarDay { + int dayNumber; + boolean isActive; + boolean isCurrent; + + CalendarDay(int dayNumber, boolean isActive, boolean isCurrent) { + this.dayNumber = dayNumber; + this.isActive = isActive; + this.isCurrent = isCurrent; + } + } + + private class CalendarAdapter extends RecyclerView.Adapter { + private final List days; + + CalendarAdapter(List days) { + this.days = days; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_calendar_day, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + CalendarDay day = days.get(position); + + if (day.dayNumber == 0) { + holder.tvDayNumber.setText(""); + holder.dayBackground.setVisibility(View.GONE); + holder.streakConnector.setVisibility(View.GONE); + } else { + holder.tvDayNumber.setText(String.valueOf(day.dayNumber)); + + if (day.isActive) { + holder.dayBackground.setVisibility(View.VISIBLE); + holder.tvDayNumber.setTextColor(getResources().getColor(R.color.white)); + + // Simple logic for streak connection: if previous day was also active + if (position > 0 && days.get(position-1).isActive && day.dayNumber > 1) { + holder.streakConnector.setVisibility(View.VISIBLE); + } else { + holder.streakConnector.setVisibility(View.GONE); + } + } else { + holder.dayBackground.setVisibility(View.GONE); + holder.streakConnector.setVisibility(View.GONE); + holder.tvDayNumber.setTextColor(getResources().getColor(R.color.text_primary)); + } + + if (day.isCurrent) { + holder.dayIndicator.setVisibility(View.VISIBLE); + } else { + holder.dayIndicator.setVisibility(View.GONE); + } + } + } + + @Override + public int getItemCount() { + return days.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + TextView tvDayNumber; + View dayBackground; + View streakConnector; + View dayIndicator; + + ViewHolder(View view) { + super(view); + tvDayNumber = view.findViewById(R.id.tvDayNumber); + dayBackground = view.findViewById(R.id.dayBackground); + streakConnector = view.findViewById(R.id.streakConnector); + dayIndicator = view.findViewById(R.id.dayIndicator); + } + } + } +} diff --git a/app/src/main/java/com/fluxup/app/Task.java b/app/src/main/java/com/fluxup/app/Task.java new file mode 100644 index 0000000..476ea5a --- /dev/null +++ b/app/src/main/java/com/fluxup/app/Task.java @@ -0,0 +1,19 @@ +package com.fluxup.app; + +public class Task { + public String id; + public String title; + public boolean completed; + public int xpReward; + public String userId; + + public Task() {} + + public Task(String id, String title, int xpReward, String userId) { + this.id = id; + this.title = title; + this.completed = false; + this.xpReward = xpReward; + this.userId = userId; + } +} diff --git a/app/src/main/java/com/fluxup/app/TrophiesActivity.java b/app/src/main/java/com/fluxup/app/TrophiesActivity.java new file mode 100644 index 0000000..13ed051 --- /dev/null +++ b/app/src/main/java/com/fluxup/app/TrophiesActivity.java @@ -0,0 +1,145 @@ +package com.fluxup.app; + +import android.os.Bundle; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.ScaleAnimation; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.firestore.ListenerRegistration; + +public class TrophiesActivity extends AppCompatActivity { + + private TextView tvDivisionTitle, tvTimeRemaining, tvTrophyProgress, tvMotivational; + private ImageButton btnBack; + private LinearLayout trophyContainer, inactiveState; + private ImageView trophy1, trophy2, trophy3; + + private FirestoreManager firestoreManager; + private FirebaseAuth mAuth; + private ListenerRegistration userListener; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_trophies); + + firestoreManager = FirestoreManager.getInstance(); + mAuth = FirebaseAuth.getInstance(); + + initViews(); + setupListeners(); + observeUserData(); + } + + private void initViews() { + tvDivisionTitle = findViewById(R.id.tvDivisionTitle); + tvTimeRemaining = findViewById(R.id.tvTimeRemaining); + tvTrophyProgress = findViewById(R.id.tvTrophyProgress); + tvMotivational = findViewById(R.id.tvMotivational); + btnBack = findViewById(R.id.btnBack); + trophyContainer = findViewById(R.id.trophyContainer); + inactiveState = findViewById(R.id.inactiveState); + trophy1 = findViewById(R.id.trophy1); + trophy2 = findViewById(R.id.trophy2); + trophy3 = findViewById(R.id.trophy3); + } + + private void setupListeners() { + btnBack.setOnClickListener(v -> finish()); + } + + private void observeUserData() { + String uid = mAuth.getUid(); + if (uid != null) { + userListener = firestoreManager.observeUser(uid, this::updateUI); + } + } + + private void updateUI(Usuario user) { + if (user == null) return; + + tvDivisionTitle.setText("Divisão " + user.league); + + // Trophy logic based on streak + int currentStreak = user.streak; + int trophies = user.trophiesCount; + + // Update trophy visuals + updateTrophyIcons(trophies); + + // Progress message + int daysToNext = 30 - (currentStreak % 30); + if (daysToNext == 30 && currentStreak > 0) { + tvTrophyProgress.setText("Troféu conquistado! Mantém a ofensiva."); + } else { + tvTrophyProgress.setText("Faltam " + daysToNext + " dias para o próximo troféu"); + } + + // Handle inactive state + if (currentStreak == 0) { + inactiveState.setVisibility(View.VISIBLE); + trophyContainer.setAlpha(0.5f); + tvMotivational.setVisibility(View.GONE); + } else { + inactiveState.setVisibility(View.GONE); + trophyContainer.setAlpha(1.0f); + tvMotivational.setVisibility(View.VISIBLE); + tvMotivational.setText("Estás a progredir bem na Divisão " + user.league + "!"); + } + + // Simple scale animation for the active trophy + animateActiveTrophy(trophies); + } + + private void updateTrophyIcons(int count) { + // Reset alphas + trophy1.setAlpha(0.3f); + trophy2.setAlpha(0.3f); + trophy3.setAlpha(0.3f); + + if (count >= 1) trophy1.setAlpha(1.0f); + if (count >= 2) trophy2.setAlpha(1.0f); + if (count >= 3) trophy3.setAlpha(1.0f); + + // Highlight current progress (the next one) + if (count == 0) highlightTrophy(trophy1); + else if (count == 1) highlightTrophy(trophy2); + else if (count == 2) highlightTrophy(trophy3); + } + + private void highlightTrophy(ImageView trophy) { + trophy.setAlpha(0.6f); + trophy.setBackgroundResource(R.drawable.circle_bg); + trophy.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getResources().getColor(R.color.primary_purple))); + trophy.setPadding(12, 12, 12, 12); + } + + private void animateActiveTrophy(int count) { + ImageView target = null; + if (count == 0) target = trophy1; + else if (count == 1) target = trophy2; + else if (count == 2) target = trophy3; + + if (target != null) { + ScaleAnimation scale = new ScaleAnimation(1f, 1.1f, 1f, 1.1f, + Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + scale.setDuration(1000); + scale.setRepeatCount(Animation.INFINITE); + scale.setRepeatMode(Animation.REVERSE); + target.startAnimation(scale); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (userListener != null) { + userListener.remove(); + } + } +} diff --git a/app/src/main/java/com/fluxup/app/Usuario.java b/app/src/main/java/com/fluxup/app/Usuario.java index 2b4044a..18f2664 100644 --- a/app/src/main/java/com/fluxup/app/Usuario.java +++ b/app/src/main/java/com/fluxup/app/Usuario.java @@ -17,6 +17,7 @@ public class Usuario { public String handle = ""; public String bio = ""; public int achievementsCount = 0; + public int trophiesCount = 0; public Usuario() {} diff --git a/app/src/main/res/drawable/button_primary.xml b/app/src/main/res/drawable/button_primary.xml index e7a329d..2e1c278 100644 --- a/app/src/main/res/drawable/button_primary.xml +++ b/app/src/main/res/drawable/button_primary.xml @@ -1,5 +1,10 @@ - - - \ No newline at end of file + + + + + + + + diff --git a/app/src/main/res/drawable/card_duo.xml b/app/src/main/res/drawable/card_duo.xml index 0d59d1b..068630f 100644 --- a/app/src/main/res/drawable/card_duo.xml +++ b/app/src/main/res/drawable/card_duo.xml @@ -1,6 +1,7 @@ - - - \ No newline at end of file + + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..2d756e9 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_contacts.xml b/app/src/main/res/drawable/ic_contacts.xml new file mode 100644 index 0000000..0f91cfa --- /dev/null +++ b/app/src/main/res/drawable/ic_contacts.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_flame.xml b/app/src/main/res/drawable/ic_flame.xml index 3a12f9e..c0a6ddb 100644 --- a/app/src/main/res/drawable/ic_flame.xml +++ b/app/src/main/res/drawable/ic_flame.xml @@ -1,9 +1,10 @@ + + android:viewportWidth="24" + android:viewportHeight="24"> - \ No newline at end of file + android:fillColor="@color/streak_orange" + android:pathData="M13.5,0.67C13.5,0.67 14.92,3.42 14.92,5.31C14.92,7.34 13.78,8.23 12.33,9.72L12.33,4.01C12.33,4.01 10.97,1.69 8.01,3.42C5.05,5.15 6.09,10.22 6.09,10.22C6.09,10.22 4.19,7.67 2,12.31C-0.19,16.95 2.13,21.82 2.13,21.82C2.13,21.82 4.1,23.33 8.35,23.33C12.6,23.33 14,21 15.35,23.33C16.7,25.66 19.3,23.33 20,22C20.7,20.67 22,17.33 22,14.67C22,12 18,10 18,10C18,10 20.35,11.38 20.35,13.62C20.35,15.86 19.31,16.5 17.5,14.67C15.69,12.84 16,9 16,9C16,9 18.66,10 18.66,7C18.66,4 14,0.67 13.5,0.67Z" /> + diff --git a/app/src/main/res/drawable/ic_nav_trophy.xml b/app/src/main/res/drawable/ic_nav_trophy.xml new file mode 100644 index 0000000..8987704 --- /dev/null +++ b/app/src/main/res/drawable/ic_nav_trophy.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000..62593c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000..453dabe --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sleeping_char.xml b/app/src/main/res/drawable/ic_sleeping_char.xml new file mode 100644 index 0000000..00a0e05 --- /dev/null +++ b/app/src/main/res/drawable/ic_sleeping_char.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_trophy_bronze.xml b/app/src/main/res/drawable/ic_trophy_bronze.xml new file mode 100644 index 0000000..48b68fc --- /dev/null +++ b/app/src/main/res/drawable/ic_trophy_bronze.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_trophy_gold.xml b/app/src/main/res/drawable/ic_trophy_gold.xml new file mode 100644 index 0000000..fae7645 --- /dev/null +++ b/app/src/main/res/drawable/ic_trophy_gold.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_trophy_silver.xml b/app/src/main/res/drawable/ic_trophy_silver.xml new file mode 100644 index 0000000..092c606 --- /dev/null +++ b/app/src/main/res/drawable/ic_trophy_silver.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/node_circle_bg.xml b/app/src/main/res/drawable/node_circle_bg.xml new file mode 100644 index 0000000..6f4545a --- /dev/null +++ b/app/src/main/res/drawable/node_circle_bg.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/node_progress_ring.xml b/app/src/main/res/drawable/node_progress_ring.xml new file mode 100644 index 0000000..73b8f6a --- /dev/null +++ b/app/src/main/res/drawable/node_progress_ring.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/progress_bar_duo.xml b/app/src/main/res/drawable/progress_bar_duo.xml index e7a329d..532c355 100644 --- a/app/src/main/res/drawable/progress_bar_duo.xml +++ b/app/src/main/res/drawable/progress_bar_duo.xml @@ -1,5 +1,17 @@ - - - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/timer_circle_bg.xml b/app/src/main/res/drawable/timer_circle_bg.xml index bb5d5e2..93bd92c 100644 --- a/app/src/main/res/drawable/timer_circle_bg.xml +++ b/app/src/main/res/drawable/timer_circle_bg.xml @@ -1,5 +1,11 @@ - - \ No newline at end of file + + + + diff --git a/app/src/main/res/layout/activity_find_friends.xml b/app/src/main/res/layout/activity_find_friends.xml new file mode 100644 index 0000000..1de8bf0 --- /dev/null +++ b/app/src/main/res/layout/activity_find_friends.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_streak.xml b/app/src/main/res/layout/activity_streak.xml index 12e93db..7b7676e 100644 --- a/app/src/main/res/layout/activity_streak.xml +++ b/app/src/main/res/layout/activity_streak.xml @@ -1,12 +1,251 @@ - + android:background="@color/background_light"> - + android:background="@color/card_background" + app:elevation="0dp"> - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_trophies.xml b/app/src/main/res/layout/activity_trophies.xml new file mode 100644 index 0000000..fdd4743 --- /dev/null +++ b/app/src/main/res/layout/activity_trophies.xml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml index 4f254e0..f6aac32 100644 --- a/app/src/main/res/layout/fragment_profile.xml +++ b/app/src/main/res/layout/fragment_profile.xml @@ -170,13 +170,17 @@ + app:contentPadding="16dp" + android:clickable="true" + android:focusable="true" + android:foreground="?attr/selectableItemBackground"> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_day_node.xml b/app/src/main/res/layout/item_day_node.xml index 1cf2e7a..b8c3a6b 100644 --- a/app/src/main/res/layout/item_day_node.xml +++ b/app/src/main/res/layout/item_day_node.xml @@ -1,41 +1,67 @@ - - - - + android:layout_width="4dp" + android:layout_height="20dp" + android:background="@color/border_color" /> - + + + + + + + + + + + + + + + android:layout_width="4dp" + android:layout_height="20dp" + android:background="@color/border_color" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_friend_suggestion.xml b/app/src/main/res/layout/item_friend_suggestion.xml new file mode 100644 index 0000000..440c20c --- /dev/null +++ b/app/src/main/res/layout/item_friend_suggestion.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index 6fdda01..77fc9e1 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -4,6 +4,10 @@ android:id="@+id/nav_inicio" android:icon="@drawable/ic_nav_home" android:title="Início" /> + + + + + + #0F172A + #1F2937 + + #FFFFFF + #9CA3AF + #374151 + + #0F172A + + + #7C3AED + #6D28D9 + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..aea0b49 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,18 @@ + + + +