diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml
index dbc65bb..b38b9b7 100644
--- a/.idea/caches/deviceStreaming.xml
+++ b/.idea/caches/deviceStreaming.xml
@@ -10,7 +10,7 @@
-
+
@@ -34,7 +34,7 @@
-
+
@@ -46,7 +46,7 @@
-
+
@@ -58,7 +58,7 @@
-
+
@@ -70,7 +70,7 @@
-
+
@@ -94,7 +94,7 @@
-
+
@@ -155,7 +155,7 @@
-
+
@@ -191,7 +191,7 @@
-
+
@@ -227,7 +227,7 @@
-
+
@@ -239,7 +239,7 @@
-
+
@@ -251,7 +251,7 @@
-
+
@@ -275,7 +275,7 @@
-
+
@@ -287,7 +287,7 @@
-
+
@@ -299,7 +299,7 @@
-
+
@@ -311,7 +311,7 @@
-
+
@@ -335,7 +335,7 @@
-
+
@@ -347,7 +347,7 @@
-
+
@@ -359,7 +359,7 @@
-
+
@@ -371,7 +371,7 @@
-
+
@@ -383,7 +383,7 @@
-
+
@@ -395,7 +395,7 @@
-
+
@@ -407,7 +407,7 @@
-
+
@@ -419,7 +419,7 @@
-
+
@@ -548,6 +548,7 @@
+
@@ -555,6 +556,11 @@
+
@@ -575,7 +581,7 @@
-
+
@@ -587,7 +593,7 @@
-
+
@@ -632,7 +638,6 @@
-
@@ -640,17 +645,11 @@
-
-
-
-
-
-
@@ -658,11 +657,6 @@
-
-
-
-
-
@@ -671,7 +665,7 @@
-
+
@@ -683,7 +677,7 @@
-
+
@@ -695,7 +689,7 @@
-
+
@@ -719,7 +713,7 @@
-
+
@@ -752,7 +746,6 @@
-
@@ -760,17 +753,11 @@
-
-
-
-
-
-
@@ -778,11 +765,6 @@
-
-
-
-
-
@@ -803,7 +785,7 @@
-
+
@@ -952,10 +934,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -963,6 +958,11 @@
+
+
+
+
+
@@ -971,7 +971,7 @@
-
+
@@ -983,7 +983,7 @@
-
+
@@ -1007,7 +1007,7 @@
-
+
@@ -1019,7 +1019,7 @@
-
+
@@ -1031,7 +1031,7 @@
-
+
@@ -1081,7 +1081,7 @@
-
+
@@ -1093,7 +1093,7 @@
-
+
@@ -1105,7 +1105,7 @@
-
+
@@ -1258,6 +1258,7 @@
+
@@ -1265,6 +1266,11 @@
+
+
+
+
+
@@ -1273,7 +1279,7 @@
-
+
@@ -1285,7 +1291,7 @@
-
+
@@ -1309,7 +1315,7 @@
-
+
@@ -1333,7 +1339,7 @@
-
+
@@ -1354,6 +1360,7 @@
+
@@ -1361,11 +1368,17 @@
+
+
+
+
+
+
@@ -1373,6 +1386,11 @@
+
+
+
+
+
@@ -1410,6 +1428,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1441,7 +1471,7 @@
-
+
@@ -1467,7 +1497,7 @@
-
+
@@ -1479,7 +1509,7 @@
-
+
@@ -1536,6 +1566,7 @@
+
@@ -1543,6 +1574,11 @@
+
+
+
+
+
@@ -1592,7 +1628,7 @@
-
+
@@ -1604,7 +1640,7 @@
-
+
@@ -1638,7 +1674,6 @@
-
@@ -1646,17 +1681,11 @@
-
-
-
-
-
-
@@ -1664,17 +1693,11 @@
-
-
-
-
-
-
@@ -1682,11 +1705,6 @@
-
-
-
-
-
@@ -1695,7 +1713,7 @@
-
+
@@ -1707,7 +1725,7 @@
-
+
diff --git a/app/src/main/java/com/fluxup/app/FirestoreManager.java b/app/src/main/java/com/fluxup/app/FirestoreManager.java
index 96a029e..e85beb1 100644
--- a/app/src/main/java/com/fluxup/app/FirestoreManager.java
+++ b/app/src/main/java/com/fluxup/app/FirestoreManager.java
@@ -4,6 +4,7 @@ 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.Calendar;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@@ -113,6 +114,15 @@ public class FirestoreManager {
updateUserStats(uid, updates, null);
}
+ /**
+ * Atualiza um campo específico do utilizador.
+ */
+ public void updateUserField(String uid, String field, Object value) {
+ java.util.Map updates = new java.util.HashMap<>();
+ updates.put(field, value);
+ updateUserStats(uid, updates);
+ }
+
/**
* Regista um log de XP.
*/
@@ -245,9 +255,9 @@ public class FirestoreManager {
if (amount != null) {
dp.xp += amount.intValue();
}
- if ("focus_task".equals(type)) {
+ if ("focus_task".equals(type) || "task_manual_completion".equals(type)) {
dp.completedTasks++;
- dp.focusSessions++;
+ if ("focus_task".equals(type)) dp.focusSessions++;
}
}
}
@@ -264,4 +274,58 @@ public class FirestoreManager {
callback.accept(new java.util.HashMap<>());
});
}
+ /**
+ * Recalculates the streak based on completed days in xp_logs.
+ */
+ public void recalculateStreakFromCompletedDays(String uid, int dailyGoal, Consumer callback) {
+ if (uid == null) {
+ callback.accept(0);
+ return;
+ }
+
+ // We'll query logs from the last 100 days to find the current consecutive streak
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DAY_OF_YEAR, -100);
+ java.util.Date startDate = cal.getTime();
+ java.util.Date endDate = new java.util.Date();
+
+ getDailyProgress(uid, startDate, endDate, dailyGoal, progressMap -> {
+ List sortedDates = new java.util.ArrayList<>(progressMap.keySet());
+ java.util.Collections.sort(sortedDates, java.util.Collections.reverseOrder());
+
+ int streak = 0;
+ java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
+ Calendar checkCal = Calendar.getInstance();
+
+ // Check today
+ String todayStr = sdf.format(checkCal.getTime());
+ DailyProgress todayDp = progressMap.get(todayStr);
+ boolean isTodayComplete = todayDp != null && todayDp.completedTasks >= dailyGoal;
+
+ if (isTodayComplete) {
+ streak = 1;
+ } else {
+ // If today is not complete, check if yesterday was complete to continue the streak
+ checkCal.add(Calendar.DAY_OF_YEAR, -1);
+ }
+
+ // Go backwards from yesterday or today
+ while (true) {
+ String dateStr = sdf.format(checkCal.getTime());
+ DailyProgress dp = progressMap.get(dateStr);
+ if (dp != null && dp.completedTasks >= dailyGoal) {
+ if (!dateStr.equals(todayStr)) streak++;
+ checkCal.add(Calendar.DAY_OF_YEAR, -1);
+ } else {
+ break; // Streak broken
+ }
+ }
+
+ // Update streak in DB
+ final int finalStreak = streak;
+ Map updates = new java.util.HashMap<>();
+ updates.put("streak", finalStreak);
+ updateUserStats(uid, updates, () -> callback.accept(finalStreak));
+ });
+ }
}
diff --git a/app/src/main/java/com/fluxup/app/InicioFragment.java b/app/src/main/java/com/fluxup/app/InicioFragment.java
index 7727b75..7b7d7e2 100644
--- a/app/src/main/java/com/fluxup/app/InicioFragment.java
+++ b/app/src/main/java/com/fluxup/app/InicioFragment.java
@@ -35,14 +35,18 @@ import androidx.core.content.ContextCompat;
public class InicioFragment extends Fragment {
- private TextView tvTimer, tvGreeting, tvMotivationalSubtitle;
+ // Focus Mode
+ private enum FocusState { NOT_STARTED, RUNNING, PAUSED }
+ private FocusState currentFocusState = FocusState.NOT_STARTED;
+
+ private TextView tvTimer, tvGreeting, tvMotivationalSubtitle, tvFocusStatus, tvFocusTitle;
private TextView tvTodayStreak, tvTodayXP, tvTodayTasksCount, tvNoTasksIncentive;
private TextView tvDailyRewardGoal;
private ProgressBar pbDailyTasksProgress;
private androidx.recyclerview.widget.RecyclerView rvTasks;
private TasksAdapter tasksAdapter;
private LinearLayout miniRankingContainer, layoutTasksSection;
- private Button btnStartFocus, btnAddTasks, btnClaimReward;
+ private Button btnStartFocus, btnSecondaryFocus, btnAddTasks, btnClaimReward;
private androidx.cardview.widget.CardView btnStreakPage;
private CountDownTimer countDownTimer;
@@ -111,20 +115,13 @@ public class InicioFragment extends Fragment {
// Focus Mode
tvTimer = view.findViewById(R.id.tvTimer);
+ tvFocusStatus = view.findViewById(R.id.tvFocusStatus);
+ tvFocusTitle = view.findViewById(R.id.tvFocusTitle);
btnStartFocus = view.findViewById(R.id.btnStartFocus);
- btnStartFocus.setOnClickListener(v -> {
- if (!isTimerRunning) {
- if (selectedTaskForFocus == null) {
- if (getContext() != null) {
- Toast.makeText(getContext(), "Seleciona uma tarefa primeiro!", Toast.LENGTH_SHORT).show();
- }
- return;
- }
- startTimer();
- } else {
- pauseTimerWithWarning();
- }
- });
+ btnSecondaryFocus = view.findViewById(R.id.btnSecondaryFocus);
+
+ btnStartFocus.setOnClickListener(v -> handleStartFocusClick());
+ btnSecondaryFocus.setOnClickListener(v -> handleSecondaryFocusClick());
// Progress
progressPathContainer = view.findViewById(R.id.progressPathContainer);
@@ -186,7 +183,7 @@ public class InicioFragment extends Fragment {
tvGreeting.setText("Olá, " + user.usuario + "!");
}
if (tvTodayStreak != null) {
- tvTodayStreak.setText(user.streak + " dias");
+ syncDailyStreak(user);
}
refreshTodayStats(user);
@@ -280,8 +277,12 @@ public class InicioFragment extends Fragment {
selectedTaskForFocus = task;
timeLeftInMillis = task.duration * 60 * 1000;
updateCountDownText();
+ updateFocusUI(FocusState.NOT_STARTED);
+ if (tvFocusTitle != null) {
+ tvFocusTitle.setText(task.title);
+ }
if (getContext() != null) {
- Toast.makeText(getContext(), "Modo Foco: " + task.title, Toast.LENGTH_SHORT).show();
+ Toast.makeText(getContext(), "Tarefa selecionada: " + task.title, Toast.LENGTH_SHORT).show();
}
}
@@ -333,11 +334,66 @@ public class InicioFragment extends Fragment {
refreshTodayStats();
}
+ private void syncDailyStreak(Usuario user) {
+ if (user == null) return;
+
+ String today = new java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()).format(new java.util.Date());
+ int completedTasksToday = user.tasks_concluidas_hoje;
+ int dailyTaskGoal = user.meta_diaria_tarefas > 0 ? user.meta_diaria_tarefas : 3;
+ int currentStreak = user.streak;
+ String lastStreakDate = user.last_streak_completed_date;
+
+ boolean willIncrement = (completedTasksToday >= dailyTaskGoal) && !today.equals(lastStreakDate);
+ int displayedStreak = 0;
+
+ if (completedTasksToday < dailyTaskGoal) {
+ displayedStreak = 0;
+ } else {
+ if (willIncrement) {
+ int newStreak = Math.max(currentStreak, 0) + 1;
+ displayedStreak = newStreak;
+
+ // Salvar no backend
+ Map updates = new HashMap<>();
+ updates.put("streak", newStreak);
+ updates.put("last_streak_completed_date", today);
+ if (newStreak > user.melhor_streak) {
+ updates.put("melhor_streak", newStreak);
+ }
+ FirestoreManager.getInstance().updateUserStats(user.id_usuario, updates);
+
+ // Atualizar objeto local para evitar loops ou UI atrasada
+ user.streak = newStreak;
+ user.last_streak_completed_date = today;
+ } else {
+ displayedStreak = currentStreak;
+ }
+ }
+
+ // Debug Logs solicitados
+ android.util.Log.d("FLUXUP_STREAK_DEBUG", "--- syncDailyStreak ---");
+ android.util.Log.d("FLUXUP_STREAK_DEBUG", "completedTasksToday = " + completedTasksToday);
+ android.util.Log.d("FLUXUP_STREAK_DEBUG", "dailyTaskGoal = " + dailyTaskGoal);
+ android.util.Log.d("FLUXUP_STREAK_DEBUG", "currentStreak = " + currentStreak);
+ android.util.Log.d("FLUXUP_STREAK_DEBUG", "lastStreakDate = " + lastStreakDate);
+ android.util.Log.d("FLUXUP_STREAK_DEBUG", "today = " + today);
+ android.util.Log.d("FLUXUP_STREAK_DEBUG", "willIncrement = " + willIncrement);
+ android.util.Log.d("FLUXUP_STREAK_DEBUG", "displayedStreak = " + displayedStreak);
+
+ if (tvTodayStreak != null) {
+ tvTodayStreak.setText(displayedStreak + " dias");
+ }
+ }
+
private void updateTodayCard(Usuario user) {
+ if (user == null) return;
int completed = user.tasks_concluidas_hoje;
int goal = user.meta_diaria_tarefas;
if (goal <= 0) goal = 3; // Default fallback
+ // Sincronizar e mostrar streak
+ syncDailyStreak(user);
+
if (tvTodayTasksCount != null) {
tvTodayTasksCount.setText(completed + "/" + goal);
}
@@ -587,14 +643,12 @@ public class InicioFragment extends Fragment {
updates.put("tasks_concluidas_hoje", com.google.firebase.firestore.FieldValue.increment(1));
updates.put("total_tasks_concluidas", com.google.firebase.firestore.FieldValue.increment(1));
- if (user.tasks_concluidas_hoje == 0) {
- updates.put("streak", com.google.firebase.firestore.FieldValue.increment(1));
- if (user.streak + 1 > user.melhor_streak) {
- updates.put("melhor_streak", user.streak + 1);
- }
- }
-
- FirestoreManager.getInstance().updateUserStats(uid, updates);
+ // Adicionar log de XP mesmo em manual para permitir recálculo se necessário
+ FirestoreManager.getInstance().addXpLog(uid, 0, "task_manual_completion");
+
+ FirestoreManager.getInstance().updateUserStats(uid, updates, () -> {
+ if (isAdded()) refreshTodayStats();
+ });
}
});
}
@@ -709,21 +763,126 @@ public class InicioFragment extends Fragment {
}
+ private void handleStartFocusClick() {
+ if (currentFocusState == FocusState.NOT_STARTED) {
+ if (selectedTaskForFocus == null) {
+ if (getContext() != null) Toast.makeText(getContext(), "Seleciona uma tarefa primeiro!", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ startTimer();
+ } else if (currentFocusState == FocusState.RUNNING) {
+ pauseTimer();
+ } else if (currentFocusState == FocusState.PAUSED) {
+ startTimer();
+ }
+ }
+
+ private void handleSecondaryFocusClick() {
+ if (currentFocusState == FocusState.RUNNING) {
+ showFinishEarlyWarning();
+ } else if (currentFocusState == FocusState.PAUSED) {
+ cancelTimer();
+ }
+ }
+
+ private void updateFocusUI(FocusState state) {
+ if (!isAdded()) return;
+ currentFocusState = state;
+
+ if (selectedTaskForFocus == null) {
+ tvFocusTitle.setText("Seleciona uma tarefa");
+ tvFocusStatus.setText("Foca no teu objetivo");
+ } else {
+ tvFocusTitle.setText(selectedTaskForFocus.title);
+ tvFocusStatus.setText("Sessão de foco");
+ }
+
+ switch (state) {
+ case NOT_STARTED:
+ tvFocusStatus.setTextColor(ContextCompat.getColor(getContext(), R.color.text_secondary));
+ btnStartFocus.setText("Começar Foco");
+ btnSecondaryFocus.setVisibility(View.GONE);
+ break;
+ case RUNNING:
+ tvFocusStatus.setText("• EM FOCO");
+ tvFocusStatus.setTextColor(ContextCompat.getColor(getContext(), R.color.primary_purple));
+ btnStartFocus.setText("Pausar");
+ btnSecondaryFocus.setText("Concluir");
+ btnSecondaryFocus.setVisibility(View.VISIBLE);
+ break;
+ case PAUSED:
+ tvFocusStatus.setText("• PAUSADO");
+ tvFocusStatus.setTextColor(android.graphics.Color.parseColor("#FFA000"));
+ btnStartFocus.setText("Continuar");
+ btnSecondaryFocus.setText("Cancelar");
+ btnSecondaryFocus.setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+
private void startTimer() {
if (selectedTaskForFocus == null) return;
+
+ if (countDownTimer != null) countDownTimer.cancel();
+
countDownTimer = new CountDownTimer(timeLeftInMillis, 1000) {
@Override
public void onTick(long millisUntilFinished) {
timeLeftInMillis = millisUntilFinished;
updateCountDownText();
}
+
@Override
public void onFinish() {
handleFocusComplete();
}
}.start();
+
isTimerRunning = true;
- btnStartFocus.setText("Pausar Foco");
+ updateFocusUI(FocusState.RUNNING);
+ }
+
+ private void pauseTimer() {
+ if (countDownTimer != null) {
+ countDownTimer.cancel();
+ }
+ isTimerRunning = false;
+ updateFocusUI(FocusState.PAUSED);
+ }
+
+ private void cancelTimer() {
+ new com.google.android.material.dialog.MaterialAlertDialogBuilder(getContext())
+ .setTitle("Cancelar Foco")
+ .setMessage("Tens a certeza que queres cancelar? Não ganharás XP.")
+ .setNegativeButton("Não", null)
+ .setPositiveButton("Sim, cancelar", (dialog, which) -> {
+ if (countDownTimer != null) countDownTimer.cancel();
+ isTimerRunning = false;
+ selectedTaskForFocus = null;
+ timeLeftInMillis = 25 * 60 * 1000;
+ updateCountDownText();
+ updateFocusUI(FocusState.NOT_STARTED);
+ })
+ .show();
+ }
+
+ private void showFinishEarlyWarning() {
+ new com.google.android.material.dialog.MaterialAlertDialogBuilder(getContext())
+ .setTitle("Concluir cedo?")
+ .setMessage("Ainda não terminaste o tempo. Se concluíres agora, não ganharás o bónus de XP.")
+ .setNegativeButton("Continuar Foco", null)
+ .setPositiveButton("Concluir sem bónus", (dialog, which) -> {
+ // Similar to cancel but maybe we still mark task as done?
+ // User said "concluir → só dá XP se terminar o tempo".
+ // I'll just reset for now as per "XP: sessão cancelada → 0 XP"
+ if (countDownTimer != null) countDownTimer.cancel();
+ isTimerRunning = false;
+ selectedTaskForFocus = null;
+ timeLeftInMillis = 25 * 60 * 1000;
+ updateCountDownText();
+ updateFocusUI(FocusState.NOT_STARTED);
+ })
+ .show();
}
private void handleFocusComplete() {
@@ -738,14 +897,15 @@ public class InicioFragment extends Fragment {
android.util.Log.d("FLUXUP_DEBUG", "TIMER_CANCELLED");
}
- isTimerRunning = false;
- if (btnStartFocus != null) btnStartFocus.setText("Começar Foco");
-
if (selectedTaskForFocus == null) {
android.util.Log.e("FLUXUP_DEBUG", "FOCUS_COMPLETE_ERROR: selectedTask null");
isCompletingFocus = false;
return;
}
+
+ isTimerRunning = false;
+ updateFocusUI(FocusState.NOT_STARTED);
+
android.util.Log.d("FLUXUP_DEBUG", "SELECTED_TASK: " + selectedTaskForFocus.id);
FirebaseUser currentUser = AuthManager.getInstance().getCurrentUser();
@@ -790,13 +950,6 @@ public class InicioFragment extends Fragment {
updates.put("tasks_concluidas_hoje", com.google.firebase.firestore.FieldValue.increment(1));
updates.put("total_tasks_concluidas", com.google.firebase.firestore.FieldValue.increment(1));
- if (user.tasks_concluidas_hoje == 0) {
- updates.put("streak", com.google.firebase.firestore.FieldValue.increment(1));
- if (user.streak + 1 > user.melhor_streak) {
- updates.put("melhor_streak", user.streak + 1);
- }
- }
-
int currentLevel = user.level;
int threshold = (currentLevel * (currentLevel + 1) / 2) * 100;
if (user.xp + 50 >= threshold) {
@@ -816,6 +969,7 @@ public class InicioFragment extends Fragment {
selectedTaskForFocus = null;
timeLeftInMillis = 25 * 60 * 1000;
updateCountDownText();
+ updateFocusUI(FocusState.NOT_STARTED);
android.util.Log.d("FLUXUP_DEBUG", "FOCUS_COMPLETE_SUCCESS");
}
isCompletingFocus = false;
@@ -833,21 +987,6 @@ public class InicioFragment extends Fragment {
}
}
- private void pauseTimerWithWarning() {
- new com.google.android.material.dialog.MaterialAlertDialogBuilder(getContext())
- .setTitle("Sair do Foco?")
- .setMessage("Se saíres agora, não ganharás o XP de foco.")
- .setNegativeButton("Continuar Focado", null)
- .setPositiveButton("Sair", (dialog, which) -> {
- if (countDownTimer != null) countDownTimer.cancel();
- isTimerRunning = false;
- btnStartFocus.setText("Começar Foco");
- timeLeftInMillis = 25 * 60 * 1000;
- updateCountDownText();
- })
- .show();
- }
-
private void updateCountDownText() {
int minutes = (int) (timeLeftInMillis / 1000) / 60;
diff --git a/app/src/main/java/com/fluxup/app/TrophiesActivity.java b/app/src/main/java/com/fluxup/app/TrophiesActivity.java
index 810abaa..bff931f 100644
--- a/app/src/main/java/com/fluxup/app/TrophiesActivity.java
+++ b/app/src/main/java/com/fluxup/app/TrophiesActivity.java
@@ -14,19 +14,25 @@ import android.widget.ProgressBar;
import java.util.Calendar;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.ListenerRegistration;
+import android.graphics.Color;
public class TrophiesActivity extends AppCompatActivity {
- private TextView tvLeagueName, tvLeagueTimeRemaining, tvLeagueObjective;
- private TextView tvUserRankingStatus, tvXPToNextPosition, tvPastPerformance;
- private ProgressBar pbWeeklyProgress;
- private LinearLayout rankingContainer;
- private ImageButton btnBack;
+ private TextView tvHeaderTimeLeft, tvHeaderXP;
+ private ImageView ivHeaderLeagueIcon;
+ private ProgressBar pbHeaderProgress;
+
+ private TextView tvUserName, tvUserDivision;
+ private ProgressBar pbUserBottom;
+ private LinearLayout divisionsContainer, rankingContainer;
+ private ImageButton btnBack, btnHelp;
private Button btnEarnXpNow;
+ private com.google.android.material.tabs.TabLayout tabLayout;
+ private View tabDivisoes, tabRanking;
private FirestoreManager firestoreManager;
private FirebaseAuth mAuth;
- private ListenerRegistration rankingListener, userListener;
+ private ListenerRegistration userListener, rankingListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -39,63 +45,171 @@ public class TrophiesActivity extends AppCompatActivity {
initViews();
setupListeners();
observeUserData();
- observeRanking("global");
}
private void initViews() {
- tvLeagueName = findViewById(R.id.tvLeagueName);
- tvLeagueTimeRemaining = findViewById(R.id.tvLeagueTimeRemaining);
- tvLeagueObjective = findViewById(R.id.tvLeagueObjective);
- tvUserRankingStatus = findViewById(R.id.tvUserRankingStatus);
- tvXPToNextPosition = findViewById(R.id.tvXPToNextPosition);
- tvPastPerformance = findViewById(R.id.tvPastPerformance);
- pbWeeklyProgress = findViewById(R.id.pbWeeklyProgress);
+ ivHeaderLeagueIcon = findViewById(R.id.ivHeaderLeagueIcon);
+ tvHeaderTimeLeft = findViewById(R.id.tvHeaderTimeLeft);
+ tvHeaderXP = findViewById(R.id.tvHeaderXP);
+ pbHeaderProgress = findViewById(R.id.pbHeaderProgress);
+
+ tvUserName = findViewById(R.id.tvUserName);
+ tvUserDivision = findViewById(R.id.tvUserDivision);
+ pbUserBottom = findViewById(R.id.pbUserBottom);
+
+ divisionsContainer = findViewById(R.id.divisionsContainer);
rankingContainer = findViewById(R.id.rankingContainer);
btnBack = findViewById(R.id.btnBack);
+ btnHelp = findViewById(R.id.btnHelp);
btnEarnXpNow = findViewById(R.id.btnEarnXpNow);
+
+ tabLayout = findViewById(R.id.tabLayout);
+ tabDivisoes = findViewById(R.id.tabDivisoes);
+ tabRanking = findViewById(R.id.tabRanking);
updateTimeRemaining();
}
private void setupListeners() {
btnBack.setOnClickListener(v -> finish());
- btnEarnXpNow.setOnClickListener(v -> finish()); // Go back to Home
+ btnHelp.setOnClickListener(v -> {
+ // Show help dialog or info
+ });
+ btnEarnXpNow.setOnClickListener(v -> finish());
- findViewById(R.id.btnRankingGlobal).setOnClickListener(v -> observeRanking("global"));
- findViewById(R.id.btnRankingAmigos).setOnClickListener(v -> observeRanking("friends"));
+ tabLayout.addOnTabSelectedListener(new com.google.android.material.tabs.TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(com.google.android.material.tabs.TabLayout.Tab tab) {
+ int position = tab.getPosition();
+ if (position == 0) {
+ tabDivisoes.setVisibility(View.VISIBLE);
+ tabRanking.setVisibility(View.GONE);
+ } else if (position == 1) {
+ tabDivisoes.setVisibility(View.GONE);
+ tabRanking.setVisibility(View.VISIBLE);
+ observeRanking("friends");
+ } else if (position == 2) {
+ tabDivisoes.setVisibility(View.GONE);
+ tabRanking.setVisibility(View.VISIBLE);
+ observeRanking("global");
+ }
+ }
+ @Override
+ public void onTabUnselected(com.google.android.material.tabs.TabLayout.Tab tab) {}
+ @Override
+ public void onTabReselected(com.google.android.material.tabs.TabLayout.Tab tab) {}
+ });
}
private void updateTimeRemaining() {
- // Simple logic for week end (Sunday 23:59)
Calendar now = Calendar.getInstance();
int daysLeft = Calendar.SUNDAY - now.get(Calendar.DAY_OF_WEEK);
if (daysLeft < 0) daysLeft += 7;
- int hoursLeft = 23 - now.get(Calendar.HOUR_OF_DAY);
- tvLeagueTimeRemaining.setText(daysLeft + " dias " + hoursLeft + "h restantes");
-
- int progress = (7 - daysLeft) * 100 / 7;
- animateProgressBar(progress);
+ tvHeaderTimeLeft.setText("Termina em " + daysLeft + " dias");
}
private void observeUserData() {
String uid = mAuth.getUid();
if (uid != null) {
- userListener = firestoreManager.observeUser(uid, this::updateUserUI);
+ userListener = firestoreManager.observeUser(uid, user -> {
+ if (user != null) {
+ updateUI(user);
+ populateDivisionsPath(user.xp);
+ }
+ });
}
}
- private void updateUserUI(Usuario user) {
- if (user == null) return;
- tvLeagueName.setText("Divisão " + user.league);
- tvLeagueObjective.setText("Fica no TOP 3 para subir para " + getNextLeague(user.league));
+ private void updateUI(Usuario user) {
+ LeagueHelper.LeagueInfo current = LeagueHelper.getCurrentLeague(user.xp);
+ LeagueHelper.LeagueInfo next = LeagueHelper.getNextLeague(user.xp);
+
+ // Header
+ ivHeaderLeagueIcon.setImageResource(current.iconRes);
+ if (next != null) {
+ int progress = (user.xp - current.minXp) * 100 / (next.minXp - current.minXp);
+ pbHeaderProgress.setProgress(progress);
+ tvHeaderXP.setText(user.xp + " / " + next.minXp + " XP");
+ } else {
+ pbHeaderProgress.setProgress(100);
+ tvHeaderXP.setText(user.xp + " XP (Lenda)");
+ }
+
+ // Bottom Card
+ tvUserName.setText(user.usuario);
+ tvUserDivision.setText(current.name + " • " + user.xp + " XP");
+ if (next != null) {
+ int bottomProgress = (user.xp - current.minXp) * 100 / (next.minXp - current.minXp);
+ pbUserBottom.setProgress(bottomProgress);
+ } else {
+ pbUserBottom.setProgress(100);
+ }
+
+ // Update user league in Firestore if it changed
+ if (!current.name.equals(user.league)) {
+ firestoreManager.updateUserField(user.id_usuario, "league", current.name);
+ }
}
- private String getNextLeague(String current) {
- switch (current) {
- case "Bronze": return "Prata";
- case "Prata": return "Ouro";
- case "Ouro": return "Platina";
- default: return "Diamante";
+ private void populateDivisionsPath(int totalXp) {
+ divisionsContainer.removeAllViews();
+ LeagueHelper.LeagueInfo current = LeagueHelper.getCurrentLeague(totalXp);
+
+ for (LeagueHelper.LeagueInfo league : LeagueHelper.LEAGUES) {
+ View view = getLayoutInflater().inflate(R.layout.item_division_card, divisionsContainer, false);
+
+ TextView tvStatus = view.findViewById(R.id.tvDivisionStatus);
+ ImageView ivIcon = view.findViewById(R.id.ivDivisionIcon);
+ TextView tvName = view.findViewById(R.id.tvDivisionName);
+ TextView tvXP = view.findViewById(R.id.tvDivisionXP);
+ TextView tvProgressText = view.findViewById(R.id.tvDivisionProgressText);
+ TextView tvPercentage = view.findViewById(R.id.tvDivisionPercentage);
+ TextView tvRewards = view.findViewById(R.id.tvRewardsList);
+ LinearLayout llBg = view.findViewById(R.id.llDivisionBg);
+ com.google.android.material.card.MaterialCardView card = view.findViewById(R.id.cardDivision);
+
+ ivIcon.setImageResource(league.iconRes);
+ tvName.setText(league.name);
+ tvXP.setText(league.minXp + (league.maxXp < 100000 ? " - " + league.maxXp : "+") + " XP");
+ tvRewards.setText(league.rewards);
+
+ if (league.name.equals(current.name)) {
+ // Current League
+ tvStatus.setVisibility(View.VISIBLE);
+ tvStatus.setText("Liga Atual");
+ tvProgressText.setVisibility(View.VISIBLE);
+ tvPercentage.setVisibility(View.VISIBLE);
+
+ LeagueHelper.LeagueInfo next = LeagueHelper.getNextLeague(totalXp);
+ if (next != null) {
+ int needed = next.minXp - totalXp;
+ tvProgressText.setText("Faltam " + needed + " XP para " + next.name);
+ int perc = (totalXp - league.minXp) * 100 / (next.minXp - league.minXp);
+ tvPercentage.setText(perc + "% até à próxima divisão");
+ } else {
+ tvProgressText.setText("Nível máximo atingido! 🔥");
+ tvPercentage.setVisibility(View.GONE);
+ }
+
+ card.setCardElevation(8f);
+ card.setStrokeWidth(4);
+ card.setStrokeColor(Color.parseColor(league.colorHex));
+ llBg.setBackgroundColor(Color.parseColor("#15" + league.colorHex.substring(1))); // ~8% opacity
+ } else if (totalXp > league.maxXp) {
+ // Completed
+ tvStatus.setVisibility(View.VISIBLE);
+ tvStatus.setText("Concluída ✅");
+ tvStatus.setTextColor(Color.GRAY);
+ card.setAlpha(0.6f);
+ } else {
+ // Locked
+ tvStatus.setVisibility(View.VISIBLE);
+ tvStatus.setText("Bloqueada 🔒");
+ tvStatus.setTextColor(Color.GRAY);
+ card.setAlpha(0.4f);
+ }
+
+ divisionsContainer.addView(view);
}
}
@@ -113,110 +227,38 @@ public class TrophiesActivity extends AppCompatActivity {
int position = 1;
String myUid = mAuth.getUid();
- Usuario me = null;
- Usuario next = null;
- int myPos = -1;
for (com.google.firebase.firestore.DocumentSnapshot doc : snapshots.getDocuments()) {
Usuario user = doc.toObject(Usuario.class);
if (user == null) continue;
-
- if (user.id_usuario.equals(myUid)) {
- me = user;
- myPos = position;
- } else if (me == null) {
- next = user; // The one above me
- }
-
addRankingItem(position++, user, myUid);
-
- // Add zone separators
- if (position == 4) addZoneSeparator("🟢 ZONA DE PROMOÇÃO", R.color.success_green);
- if (position == 11) addZoneSeparator("⚪ ZONA DE MANUTENÇÃO", R.color.text_secondary);
- if (position == 16) addZoneSeparator("🔴 ZONA DE DESPROMOÇÃO", R.color.error_red);
- }
-
- if (me != null) {
- updateUserProgressCard(myPos, me, next);
}
});
}
- private void animateProgressBar(int progress) {
- android.animation.ObjectAnimator animation = android.animation.ObjectAnimator.ofInt(pbWeeklyProgress, "progress", 0, progress);
- animation.setDuration(1500);
- animation.setInterpolator(new android.view.animation.DecelerateInterpolator());
- animation.start();
- }
-
private void addRankingItem(int pos, Usuario user, String myUid) {
View view = getLayoutInflater().inflate(R.layout.item_ranking_user, rankingContainer, false);
-
TextView tvPos = view.findViewById(R.id.tvRankPosition);
TextView tvName = view.findViewById(R.id.tvRankingName);
TextView tvXP = view.findViewById(R.id.tvRankingXP);
TextView tvTu = view.findViewById(R.id.tvRankingLabelTu);
- ImageView ivAvatar = view.findViewById(R.id.ivRankingAvatar);
androidx.cardview.widget.CardView card = view.findViewById(R.id.cardRankingUser);
- if (pos == 1) {
- tvPos.setText("🥇");
- tvPos.setTextSize(18f);
- card.setCardBackgroundColor(android.graphics.Color.parseColor("#FFFDE7")); // Light gold
- } else if (pos == 2) {
- tvPos.setText("🥈");
- tvPos.setTextSize(18f);
- } else if (pos == 3) {
- tvPos.setText("🥉");
- tvPos.setTextSize(18f);
- } else {
- tvPos.setText("#" + pos);
- }
-
+ tvPos.setText("#" + pos);
tvName.setText(user.usuario);
tvXP.setText(user.xp_semanal + " XP");
if (user.id_usuario.equals(myUid)) {
tvTu.setVisibility(View.VISIBLE);
- card.setCardBackgroundColor(android.graphics.Color.parseColor("#F3E5F5")); // Light purple
- card.setCardElevation(4f);
+ card.setCardBackgroundColor(Color.parseColor("#F3E5F5"));
}
-
rankingContainer.addView(view);
}
- private void addZoneSeparator(String text, int colorRes) {
- TextView tv = new TextView(this);
- tv.setText(text);
- tv.setTextSize(10);
- tv.setPadding(0, 24, 0, 8);
- tv.setTextColor(getResources().getColor(colorRes));
- tv.setGravity(android.view.Gravity.CENTER);
- tv.setAlpha(0.7f);
- rankingContainer.addView(tv);
- }
-
- private void updateUserProgressCard(int myPos, Usuario me, Usuario aboveMe) {
- androidx.cardview.widget.CardView cardUserProgress = findViewById(R.id.cardUserProgress);
-
- if (myPos <= 3) {
- tvUserRankingStatus.setText("🎉 Estás na zona de promoção!");
- tvUserRankingStatus.setTextColor(getResources().getColor(R.color.success_green));
- tvXPToNextPosition.setText("Mantém o foco para subir de divisão e ganhar recompensas!");
- cardUserProgress.setCardBackgroundColor(android.graphics.Color.parseColor("#E8F5E9")); // Light green
- } else {
- int needed = (aboveMe != null) ? (aboveMe.xp_semanal - me.xp_semanal + 1) : 0;
- tvUserRankingStatus.setText("Faltam " + needed + " XP para subires de posição");
- tvUserRankingStatus.setTextColor(getResources().getColor(R.color.text_primary));
- tvXPToNextPosition.setText("Estás em #" + myPos + " com " + me.xp_semanal + " XP");
- cardUserProgress.setCardBackgroundColor(getResources().getColor(R.color.white));
- }
- }
-
@Override
protected void onDestroy() {
super.onDestroy();
- if (rankingListener != null) rankingListener.remove();
if (userListener != null) userListener.remove();
+ if (rankingListener != null) rankingListener.remove();
}
}
diff --git a/app/src/main/java/com/fluxup/app/Usuario.java b/app/src/main/java/com/fluxup/app/Usuario.java
index 363a48d..90fa4af 100644
--- a/app/src/main/java/com/fluxup/app/Usuario.java
+++ b/app/src/main/java/com/fluxup/app/Usuario.java
@@ -34,6 +34,7 @@ public class Usuario {
public int sessoes_foco_completas = 0;
public int dias_ativos = 1;
public String last_active_date = ""; // YYYY-MM-DD
+ public String last_streak_completed_date = ""; // YYYY-MM-DD
public String last_reward_claim_date = ""; // YYYY-MM-DD
public String avatar_url = "";
diff --git a/app/src/main/res/layout/activity_trophies.xml b/app/src/main/res/layout/activity_trophies.xml
index 5c4178f..2e3fb6c 100644
--- a/app/src/main/res/layout/activity_trophies.xml
+++ b/app/src/main/res/layout/activity_trophies.xml
@@ -1,305 +1,274 @@
-
-
+ app:contentInsetStart="0dp">
-
+
-
-
-
+
-
+
+
+
+
+
+
+ android:padding="20dp"
+ android:gravity="center">
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:text="Termina em 3 dias"
+ android:textColor="@color/text_secondary"
+ android:textSize="14sp"
+ android:layout_marginBottom="8dp"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ android:orientation="vertical" />
-
-
+
-
+
+
+
+
+
+
+
+ android:padding="16dp">
-
-
+
-
-
-
-
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"
+ android:src="@drawable/ic_nav_profile" />
-
-
-
+
+
-
-
-
-
-
+ android:text="Utilizador"
+ android:textColor="@color/text_primary"
+ android:textStyle="bold"
+ android:textSize="16sp"/>
-
-
-
-
-
-
-
-
-
+ android:text="Bronze • 0 XP"
+ android:textColor="@color/text_secondary"
+ android:textSize="14sp"/>
-
-
+
-
-
+
-
-
+
-
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_inicio.xml b/app/src/main/res/layout/fragment_inicio.xml
index 166612f..51fff63 100644
--- a/app/src/main/res/layout/fragment_inicio.xml
+++ b/app/src/main/res/layout/fragment_inicio.xml
@@ -239,14 +239,25 @@
android:orientation="vertical">
+
+
-
+
+
+
+
+
+
diff --git a/gradle.properties b/gradle.properties
index 49c7de9..0a72118 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,4 @@
android.useAndroidX=true
+android.enableJetifier=true
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false