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 @@
+
+
+
+