melhorar o streak
This commit is contained in:
@@ -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<String, Object> 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<Integer> 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<String> 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<String, Object> updates = new java.util.HashMap<>();
|
||||
updates.put("streak", finalStreak);
|
||||
updateUserStats(uid, updates, () -> callback.accept(finalStreak));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
|
||||
@@ -1,305 +1,274 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/background_light">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/card_background"
|
||||
app:elevation="0dp">
|
||||
|
||||
<RelativeLayout
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:paddingHorizontal="16dp">
|
||||
app:contentInsetStart="0dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnBack"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_back"
|
||||
app:tint="@color/text_primary" />
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvToolbarTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="Ligas"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
</RelativeLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
<ImageButton
|
||||
android:id="@+id/btnBack"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_back"
|
||||
app:tint="@color/text_primary" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="Ligas"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnHelp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@android:drawable/ic_menu_help"
|
||||
app:tint="@color/text_primary" />
|
||||
</RelativeLayout>
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<!-- Gamified Header Section -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
android:padding="20dp"
|
||||
android:gravity="center">
|
||||
|
||||
<!-- 1. 🥇 HEADER DA LIGA -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardCornerRadius="20dp"
|
||||
app:cardBackgroundColor="@color/primary_purple"
|
||||
app:cardElevation="4dp">
|
||||
<ImageView
|
||||
android:id="@+id/ivHeaderLeagueIcon"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:src="@drawable/ic_trophy_bronze"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivLeagueBadge"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:src="@drawable/ic_trophy_bronze"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:tint="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLeagueName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Divisão Bronze"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLeagueTimeRemaining"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="2 dias 14h restantes"
|
||||
android:textColor="#E0E0E0"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pbWeeklyProgress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:max="100"
|
||||
android:progress="60"
|
||||
android:progressTint="@color/reward_yellow"
|
||||
android:progressBackgroundTint="#4DFFFFFF" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- 2. 🎯 OBJETIVO DA LIGA -->
|
||||
<TextView
|
||||
android:id="@+id/tvLeagueObjective"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🚀 Fica no TOP 3 para subir para Prata"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/primary_purple"
|
||||
android:textStyle="bold"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
|
||||
<!-- 5. ⚡ PROGRESSO DO UTILIZADOR -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardUserProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardBackgroundColor="@color/white">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUserRankingStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Faltam 120 XP para entrares no TOP 3"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textStyle="bold"
|
||||
android:textAlignment="center"
|
||||
android:textSize="16sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvXPToNextPosition"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Diferença para o próximo: 45 XP"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textAlignment="center"
|
||||
android:textSize="14sp"/>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- 8. 👥 FILTRO DE RANKING -->
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:id="@+id/toggleRankingFilter"
|
||||
android:id="@+id/tvHeaderTimeLeft"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="24dp"
|
||||
app:singleSelection="true"
|
||||
app:selectionRequired="true"
|
||||
app:checkedButton="@+id/btnRankingGlobal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnRankingGlobal"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="50dp"
|
||||
android:text="Global"
|
||||
android:textStyle="bold"
|
||||
android:textSize="14sp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnRankingAmigos"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="50dp"
|
||||
android:text="Amigos"
|
||||
android:textStyle="bold"
|
||||
android:textSize="14sp"/>
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
android:text="Termina em 3 dias"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pbHeaderProgress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="12dp"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:progressTint="@color/primary_purple"
|
||||
android:progressBackgroundTint="#E0E0E0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvHeaderXP"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="6415 / 10000 XP"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="8dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabMode="fixed"
|
||||
app:tabGravity="fill"
|
||||
app:tabIndicatorColor="@color/primary_purple"
|
||||
app:tabSelectedTextColor="@color/primary_purple"
|
||||
app:tabTextColor="@color/text_secondary">
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Divisões" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Amigos" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Ranking" />
|
||||
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/contentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/appBar"
|
||||
android:layout_above="@id/bottomWrapper">
|
||||
|
||||
<!-- DIVISÕES TAB -->
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/tabDivisoes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/divisionsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<!-- RANKING / AMIGOS TAB -->
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/tabRanking"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
android:clipToPadding="false"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 3. 📊 RANKING DA SEMANA -->
|
||||
<LinearLayout
|
||||
android:id="@+id/rankingContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="32dp"/>
|
||||
android:orientation="vertical" />
|
||||
|
||||
<!-- 6. 🎁 RECOMPENSAS DA SEMANA -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Recompensas da semana"
|
||||
android:textStyle="bold"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<LinearLayout
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottomWrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/card_background"
|
||||
android:elevation="12dp">
|
||||
|
||||
<!-- Fixed User Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cardUserProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardElevation="0dp"
|
||||
app:cardBackgroundColor="@color/card_background">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="3"
|
||||
android:layout_marginBottom="32dp">
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 1º Lugar -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="4dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardBackgroundColor="#FFF8E1"
|
||||
app:cardElevation="1dp">
|
||||
<LinearLayout
|
||||
android:id="@+id/cvAvatar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:cardElevation="0dp">
|
||||
<ImageView
|
||||
android:id="@+id/ivUserAvatar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:gravity="center">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="🥇" android:textSize="32sp" android:layout_marginBottom="8dp"/>
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="1º Lugar" android:textStyle="bold" android:textSize="14sp" android:textColor="#FF8F00"/>
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="+300 XP\nBadge" android:textAlignment="center" android:textSize="12sp" android:textColor="#FF8F00"/>
|
||||
</LinearLayout>
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/ic_nav_profile" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- 2º Lugar -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardBackgroundColor="#F5F5F5"
|
||||
app:cardElevation="1dp">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@id/cvAvatar"
|
||||
android:layout_toStartOf="@id/ivUserExpand"
|
||||
android:layout_marginStart="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUserName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:gravity="center">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="🥈" android:textSize="28sp" android:layout_marginBottom="8dp"/>
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="2º Lugar" android:textStyle="bold" android:textSize="14sp" android:textColor="#757575"/>
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="+200 XP" android:textAlignment="center" android:textSize="12sp" android:textColor="#757575"/>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
android:text="Utilizador"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textStyle="bold"
|
||||
android:textSize="16sp"/>
|
||||
|
||||
<!-- 3º Lugar -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="4dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardBackgroundColor="#EFEBE9"
|
||||
app:cardElevation="1dp">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<TextView
|
||||
android:id="@+id/tvUserDivision"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:gravity="center">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="🥉" android:textSize="28sp" android:layout_marginBottom="8dp"/>
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="3º Lugar" android:textStyle="bold" android:textSize="14sp" android:textColor="#8D6E63"/>
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="+100 XP" android:textAlignment="center" android:textSize="12sp" android:textColor="#8D6E63"/>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
android:text="Bronze • 0 XP"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp"/>
|
||||
|
||||
<!-- 7. 📈 HISTÓRICO DE PERFORMANCE -->
|
||||
<TextView
|
||||
android:id="@+id/tvPastPerformance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Semana passada: 2º lugar (subiste)"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:visibility="gone" />
|
||||
<ProgressBar
|
||||
android:id="@+id/pbUserBottom"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:progressTint="@color/primary_purple"
|
||||
android:progressBackgroundTint="#E0E0E0" />
|
||||
|
||||
<!-- 9. 🔘 BOTÃO DE ACÇÃO -->
|
||||
<Button
|
||||
android:id="@+id/btnEarnXpNow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:text="⚡ Ganhar XP Agora"
|
||||
android:backgroundTint="@color/primary_purple"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textAllCaps="false"
|
||||
app:cornerRadius="16dp"
|
||||
android:layout_marginBottom="40dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
<ImageView
|
||||
android:id="@+id/ivUserExpand"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/ic_arrow_right"
|
||||
app:tint="@color/text_secondary" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</RelativeLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnEarnXpNow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="Ganhar XP agora"
|
||||
android:backgroundTint="@color/primary_purple"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textAllCaps="false"
|
||||
app:cornerRadius="12dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -239,14 +239,25 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFocusTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="Modo Foco"
|
||||
android:text="Seleciona uma tarefa"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="22sp"
|
||||
android:textAlignment="center"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFocusStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="Sessão de foco"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp"
|
||||
android:visibility="visible" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/timerBlock"
|
||||
android:layout_width="200dp"
|
||||
@@ -281,17 +292,41 @@
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnStartFocus"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/button_primary"
|
||||
android:text="Começar Foco"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@null" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSecondaryFocus"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/button_secondary"
|
||||
android:text="Cancelar"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@null" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnStartFocus"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/button_primary"
|
||||
android:text="Começar Foco"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@null" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user